Skip to content

Commit

Permalink
Fix circular association (#47)
Browse files Browse the repository at this point in the history
* Fix circular association issue (close #36);
Adapt tests;

* Fix ignored circular association issue;
Add more tests concerning circular association;

* Minor refactor;
Fix comment;
  • Loading branch information
Chnapy authored Aug 21, 2019
1 parent f5c9461 commit 0710683
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 82 deletions.
81 changes: 62 additions & 19 deletions src/engine/AssociationEngine.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ClassLike } from '../types/ClassTypes';
import { JSONSchema7 } from 'json-schema';
import { REFLECT_KEY } from '../decorators/ReflectKeys';
import { Association, AssociationMap, ClassFn } from '../types/AssociationTypes';
import {ClassLike} from '../types/ClassTypes';
import {JSONSchema7} from 'json-schema';
import {REFLECT_KEY} from '../decorators/ReflectKeys';
import {Association, AssociationMap, ClassFn} from '../types/AssociationTypes';
import PropertyEngine from './PropertyEngine';
import SchemaEngine from './SchemaEngine';
import { NotAJsonSchemaError } from '../exception/NotAJsonSchemaError';
import { CircularDependencyError } from '../exception/CircularDependencyError';
import { Util } from './Util';
import {NotAJsonSchemaError} from '../exception/NotAJsonSchemaError';
import {Util} from './Util';

/**
* Handle all associations concerns
Expand All @@ -16,34 +15,56 @@ export default class AssociationEngine {
* Apply all associations with target as source class.
*
* @param target class source
* @param sourceStack stack of all class covered, in case of CircularDependencyError
* @param definitions schema definitions of the root schema
* @param rootTarget root schema class, if not target
*/
static computeJSONAssociations(target: ClassLike, sourceStack: ClassLike[] = []): void {
if (!Util.isClass(target)) {
throw new NotAJsonSchemaError(target);
}
static computeJSONAssociations(target: ClassLike, definitions?: JSONSchema7['definitions'], rootTarget?: ClassLike): void {

if (sourceStack.some(s => s.name === target.name)) {
throw new CircularDependencyError(...sourceStack, target);
}
const isRoot = !rootTarget;

rootTarget = rootTarget || target;

sourceStack.push(target);
if(!definitions) {
const rootTargetSchema = SchemaEngine.getReflectSchema(rootTarget) || {};

definitions = rootTargetSchema.definitions || {};
}

const assocMapClass = AssociationEngine.getAssociations(target.name, target.prototype);

assocMapClass.forEach(a => {
const assocTarget: ClassLike = a.targetFn();

const valueSchema = SchemaEngine.getComputedJSONSchema(assocTarget, sourceStack);
if (!Util.isClass(assocTarget)) {
throw new NotAJsonSchemaError(assocTarget);
}

if (assocTarget !== rootTarget) {
const targetID = AssociationEngine.generateSchemaID(assocTarget);

if (!definitions![targetID]) {
definitions![targetID] = SchemaEngine.getComputedJSONSchema(assocTarget, definitions, rootTarget);
}
}

const refSchema: JSONSchema7 = {
$ref: AssociationEngine.generateRef(assocTarget, rootTarget)
};

const value = a.jsonPropertyKey
? {
[a.jsonPropertyKey]: valueSchema
[a.jsonPropertyKey]: refSchema
}
: valueSchema;
: refSchema;

PropertyEngine.defineReflectProperties(target.prototype, a.key, value);
});

if (Object.keys(definitions).length && isRoot) {
SchemaEngine.defineReflectSchema(rootTarget, {
definitions
});
}
}

/**
Expand Down Expand Up @@ -101,6 +122,28 @@ export default class AssociationEngine {
return associationMap[className] || [];
}

/**
* Generate a '$ref' value for the given target class.
*
* @param target
* @param rootTarget
*/
static generateRef(target: ClassLike, rootTarget?: ClassLike): string {
if (target === rootTarget) {
return '#';
}
return `#/definitions/${AssociationEngine.generateSchemaID(target)}`;
}

/**
* Generate an ID for the given target class.
*
* @param target
*/
static generateSchemaID(target: ClassLike): string {
return `_${target.name}_`;
}

/**
* Return the association map of the given class prototype.
*
Expand Down
14 changes: 12 additions & 2 deletions src/engine/PropertyEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class PropertyEngine {

/**
* Return a JSON Schema for a property.
* Define schema 'type' only if no '$ref' is defined.
*
* @param reflectEntity partial schema get by reflection
* @param paramEntity partial schema given in param
Expand All @@ -50,12 +51,21 @@ export default class PropertyEngine {
paramEntity: JSONSchema7,
typeTS: ClassLike
): JSONSchema7 {

const partialEntity = {
...reflectEntity,
...paramEntity
};

if(partialEntity.$ref) {
return partialEntity;
}

const typeEntity = PropertyEngine.getJSONSchemaType(typeTS) as J;

return {
...typeEntity,
...reflectEntity,
...paramEntity
...partialEntity
};
}

Expand Down
7 changes: 4 additions & 3 deletions src/engine/SchemaEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ export default class SchemaEngine {
* Compute all the class associations.
*
* @param target JSONSchema class
* @param sourceStack stack of all class covered in associations
* @param definitions schema definitions of the root schema
* @param rootTarget root schema class, if not target
*/
static getComputedJSONSchema(target: ClassLike, sourceStack?: ClassLike[]): JSONSchema7 {
AssociationEngine.computeJSONAssociations(target, sourceStack);
static getComputedJSONSchema(target: ClassLike, definitions?: JSONSchema7['definitions'], rootTarget?: ClassLike): JSONSchema7 {
AssociationEngine.computeJSONAssociations(target, definitions, rootTarget);

const schema = SchemaEngine.getReflectSchema(target);

Expand Down
20 changes: 0 additions & 20 deletions src/exception/CircularDependencyError.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/tabbouleh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ export * from './types/ClassTypes';
export * from './types/AssociationTypes';

export * from './exception/NotAJsonSchemaError';
export * from './exception/CircularDependencyError';

export default Tabbouleh;
4 changes: 1 addition & 3 deletions src/types/JSONTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type JSONEntity<T extends JSONTypeName | JSONTypeName[], D> = Pick<
| 'description'
| 'readOnly'
| 'writeOnly'
| 'definitions'
> & {
/**
* @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-6.1
Expand All @@ -53,9 +54,6 @@ export type JSONEntity<T extends JSONTypeName | JSONTypeName[], D> = Pick<
anyOf?: Omit<JSONEntity<T, D>, 'type'>[];
not?: JSONEntity<T, D>;

// TODO find programmatic solution
// definitions

// TODO find programmatic solution
// extends

Expand Down
47 changes: 32 additions & 15 deletions test/JSONArray.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { JSONSchema7 } from 'json-schema';
import { FOOD_SCHEMA_PROPS } from './genericSample/Food.sample';
import {FOOD_SCHEMA_PROPS, FoodSample} from './genericSample/Food.sample';
import Tabbouleh from '../src/engine/Tabbouleh';
import { JSONArray1Sample } from './JSONArraySample/JSONArray1.sample';
import { JSONArray2Sample } from './JSONArraySample/JSONArray2.sample';
import { JSONArray3Sample } from './JSONArraySample/JSONArray3.sample';
import { JSONArray4Sample } from './JSONArraySample/JSONArray4.sample';
import AssociationEngine from "../src/engine/AssociationEngine";

const foodSampleID = AssociationEngine.generateSchemaID(FoodSample);

const schemaJsonArray1: JSONSchema7 = {
type: 'object',
Expand Down Expand Up @@ -33,35 +36,49 @@ const schemaJsonArray2: JSONSchema7 = {

const schemaJsonArray3: JSONSchema7 = {
type: 'object',
definitions: {
[foodSampleID]: {
type: 'object',

...FOOD_SCHEMA_PROPS,

properties: {
parsley: {
type: 'string'
}
}
}
},
properties: {
myArray: {
type: 'array',
items: {
...FOOD_SCHEMA_PROPS,
type: 'object',
properties: {
parsley: {
type: 'string'
}
}
$ref: AssociationEngine.generateRef(FoodSample)
}
}
}
};

const schemaJsonArray4: JSONSchema7 = {
type: 'object',
definitions: {
[foodSampleID]: {
type: 'object',

...FOOD_SCHEMA_PROPS,

properties: {
parsley: {
type: 'string'
}
}
}
},
properties: {
myArray: {
type: 'array',
items: {
...FOOD_SCHEMA_PROPS,
type: 'object',
properties: {
parsley: {
type: 'string'
}
}
$ref: AssociationEngine.generateRef(FoodSample)
},
minItems: 1,
uniqueItems: true
Expand Down
13 changes: 10 additions & 3 deletions test/JSONObject.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Tabbouleh from '../src/engine/Tabbouleh';
import { JSONObject1Sample, OBJECT_SAMPLE_1_USER } from './JSONObjectSample/JSONObject1.sample';
import { JSONSchema7 } from 'json-schema';
import { FOOD_SCHEMA_PROPS } from './genericSample/Food.sample';
import {FOOD_SCHEMA_PROPS, FoodSample} from './genericSample/Food.sample';
import { JSONObject2Sample } from './JSONObjectSample/JSONObject2.sample';
import { JSONObject3Sample } from './JSONObjectSample/JSONObject3.sample';
import AssociationEngine from "../src/engine/AssociationEngine";

const schemaObjectSample1: JSONSchema7 = {
type: 'object',
Expand All @@ -16,10 +17,11 @@ const schemaObjectSample1: JSONSchema7 = {
}
};

const foodSampleID = AssociationEngine.generateSchemaID(FoodSample);
const schemaObjectSample2: JSONSchema7 = {
type: 'object',
properties: {
food: {
definitions: {
[foodSampleID]: {
type: 'object',

...FOOD_SCHEMA_PROPS,
Expand All @@ -30,6 +32,11 @@ const schemaObjectSample2: JSONSchema7 = {
}
}
}
},
properties: {
food: {
$ref: AssociationEngine.generateRef(FoodSample)
}
}
};

Expand Down
Loading

0 comments on commit 0710683

Please sign in to comment.