Skip to content

Commit

Permalink
Merge 9147130 into 455eae8
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni committed Jun 7, 2021
2 parents 455eae8 + 9147130 commit aad46ef
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 57 deletions.
7 changes: 5 additions & 2 deletions docs/interpretation_of_JSON_Schema_draft_7.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The order of interpretation:
- `required` are interpreted as is.
- `patternProperties` are interpreted as is, where duplicate patterns for the model are [merged](#Merging-models).
- `additionalProperties` are interpreted as is, where duplicate additionalProperties for the model are [merged](#Merging-models). If the schema does not define `additionalProperties` it defaults to `true` schema.
- `items` are interpreted as is, where more than 1 item are [merged](#Merging-models).
- `items` are interpreted as ether tuples or simple array, where more than 1 item are [merged](#Merging-models). Usage of `items` infers `array` model type.
- `properties` are interpreted as is, where duplicate `properties` for the model are [merged](#Merging-models). Usage of `properties` infers `object` model type.
- [allOf](#allOf-sub-schemas)
- `enum` is interpreted as is, where each `enum`. Usage of `enum` infers the enumerator value type to the model, but only if the schema does not have `type` specified.
Expand Down Expand Up @@ -54,7 +54,10 @@ If only one side has a property defined, it is used as is, if both have it defin
- `additionalProperties` if both models contain it the two are recursively merged together.
- `patternProperties` if both models contain a pattern the corresponding models are recursively merged together.
- `properties` if both models contain the same property the corresponding models are recursively merged together.
- `items` if both models contain items they are recursively merged together.
- `items` are merged together based on a couple of rules:
- If both models are simple arrays those item models are merged together as is.
- If both models are tuple arrays each tuple model (at specific index) is merged together.
- If either one side is different from the other, the tuple schemas is prioritized as it is more restrictive.
- `types` if both models contain types they are merged together, duplicate types are removed.
- `enum` if both models contain enums they are merged together, duplicate enums are removed.
- `required` if both models contain required properties they are merged together, duplicate required properties are removed.
Expand Down
11 changes: 9 additions & 2 deletions src/generators/typescript/TypeScriptRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,15 @@ export abstract class TypeScriptRenderer extends AbstractRenderer<TypeScriptOpti
case 'boolean':
return 'boolean';
case 'array': {
const types = model.items ? this.renderType(model.items) : 'unknown';
return `Array<${types}>`;
//Check and see if it should be rendered as tuples or array
if (Array.isArray(model.items)) {
const types = model.items.map((item) => {
return this.renderType(item);
});
return `[${types.join(', ')}]`;
}
const arrayType = model.items ? this.renderType(model.items) : 'unknown';
return `Array<${arrayType}>`;
}
default: return type;
}
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/InterpretAdditionalProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isModelObject } from './Utils';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretAdditionalProperties(schema: Schema, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (!isModelObject(model)) {return;}
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/InterpretAllOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isModelObject } from './Utils';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretAllOf(schema: Schema, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (schema.allOf === undefined) {return;}
Expand Down
11 changes: 7 additions & 4 deletions src/interpreter/InterpretItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Interpreter, InterpreterOptions } from './Interpreter';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretItems(schema: Schema, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (schema.items === undefined) {return;}
Expand All @@ -24,12 +24,15 @@ export default function interpretItems(schema: Schema, model: CommonModel, inter
* @param itemSchemas
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
function interpretArrayItems(rootSchema: Schema, itemSchemas: (Schema | boolean)[] | (Schema | boolean), model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (Array.isArray(itemSchemas)) {
for (const itemSchema of itemSchemas) {
interpretArrayItems(rootSchema, itemSchema, model, interpreter, interpreterOptions);
for (const [index, itemSchema] of itemSchemas.entries()) {
const itemModel = interpreter.interpret(itemSchema, interpreterOptions);
if (itemModel.length > 0) {
model.addItemTuple(itemModel[0], rootSchema, index);
}
}
} else {
const itemModels = interpreter.interpret(itemSchemas, interpreterOptions);
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/InterpretNot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Interpreter, InterpreterOptions } from './Interpreter';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretNot(schema: Schema, model: CommonModel, interpreter: Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (schema.not === undefined) {return;}
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/InterpretPatternProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Interpreter, InterpreterOptions } from './Interpreter';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretPatternProperties(schema: Schema | boolean, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (typeof schema === 'boolean') {return;}
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/InterpretProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Interpreter, InterpreterOptions } from './Interpreter';
* @param schema
* @param model
* @param interpreter
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
export default function interpretProperties(schema: Schema, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (schema.properties === undefined) {return;}
Expand Down
8 changes: 4 additions & 4 deletions src/interpreter/Interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class Interpreter {
* Index > 0 will always be the separated models that the interpreter determines are fit to be on their own.
*
* @param schema
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
interpret(schema: Schema | boolean, options: InterpreterOptions = Interpreter.defaultInterpreterOptions): CommonModel[] {
if (this.seenSchemas.has(schema)) {
Expand Down Expand Up @@ -64,7 +64,7 @@ export class Interpreter {
*
* @param model
* @param schema
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
private interpretSchema(model: CommonModel, schema: Schema | boolean, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions) {
if (schema === true) {
Expand Down Expand Up @@ -106,7 +106,7 @@ export class Interpreter {
* @param schema to go through
* @param currentModel the current output
* @param rootSchema the root schema to use as original schema when merged
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
interpretAndCombineSchema(schema: (Schema | boolean) | undefined, currentModel: CommonModel, rootSchema: Schema, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (typeof schema !== 'object') {return;}
Expand All @@ -123,7 +123,7 @@ export class Interpreter {
* @param schema to go through
* @param currentModel the current output
* @param rootSchema the root schema to use as original schema when merged
* @param options to control the interpret process
* @param interpreterOptions to control the interpret process
*/
interpretAndCombineMultipleSchemas(schema: (Schema | boolean)[] | undefined, currentModel: CommonModel, rootSchema: Schema, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void {
if (!Array.isArray(schema)) { return; }
Expand Down
77 changes: 52 additions & 25 deletions src/models/CommonModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export class CommonModel extends CommonSchema<CommonModel> {
* If items already exist the two are merged.
*
* @param itemModel
* @param schema schema to the corresponding property model
* @param schema
* @param addAsArray
*/
addItem(itemModel: CommonModel, schema: Schema): void {
if (this.items !== undefined) {
Expand All @@ -123,6 +124,31 @@ export class CommonModel extends CommonSchema<CommonModel> {
}
}

/**
* Adds a tuple to the model.
*
* If a item already exist it will be merged.
*
* @param tupleModel
* @param schema
* @param index
*/
addItemTuple(tupleModel: CommonModel, schema: Schema, index: number): void {
let modelItems = this.items;
if (!Array.isArray(modelItems)) {
Logger.warn('Trying to add item tuple to a non-tuple item, will drop existing item model', tupleModel, schema, index);
modelItems = [];
}
const existingModelAtIndex = modelItems[Number(index)];
if (existingModelAtIndex !== undefined) {
Logger.warn('Trying to add item tuple at index ${index} but it was already occupied, merging models', tupleModel, schema, index);
modelItems[Number(index)] = CommonModel.mergeCommonModels(existingModelAtIndex, tupleModel, schema);
} else {
modelItems[Number(index)] = tupleModel;
}
this.items = modelItems;
}

/**
* Add enum value to the model.
*
Expand Down Expand Up @@ -359,39 +385,40 @@ export class CommonModel extends CommonSchema<CommonModel> {
}

/**
* Merge items together so only one CommonModel remains.
* Merge items together, prefer tuples over simple array since it is more strict.
*
* @param mergeTo
* @param mergeFrom
* @param originalSchema
* @param alreadyIteratedModels
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
private static mergeItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) { // NOSONAR
const merge = (models: CommonModel | CommonModel[] | undefined): CommonModel | undefined => {
if (!Array.isArray(models)) {return models;}
let mergedItemsModel: CommonModel | undefined = undefined;
for (const [index, model] of models.entries()) {
Logger.warn(`Found duplicate items at index ${index} for model. Model item for ${mergeFrom.$id || 'unknown'} merged into ${mergeTo.$id || 'unknown'}`, mergeTo, mergeFrom, originalSchema);
mergedItemsModel = CommonModel.mergeCommonModels(mergedItemsModel, model, originalSchema, alreadyIteratedModels);
}
return mergedItemsModel;
};
if (mergeFrom.items !== undefined) {
//Incase of arrays, merge them into a single model
const mergeFromItemsModel = merge(mergeFrom.items);
const mergeToItemsModel = merge(mergeTo.items);
if (mergeFromItemsModel !== undefined) {
if (mergeToItemsModel === undefined) {
mergeTo.items = mergeFromItemsModel;
} else {
Logger.warn(`Found duplicate item for model. Model item for ${mergeFrom.$id || 'unknown'} merged into ${mergeTo.$id || 'unknown'}`, mergeTo, mergeFrom, originalSchema);
mergeTo.items = CommonModel.mergeCommonModels(mergeToItemsModel, mergeFromItemsModel, originalSchema, alreadyIteratedModels);
}
private static mergeItems(mergeTo: CommonModel, mergeFrom: CommonModel, originalSchema: Schema, alreadyIteratedModels: Map<CommonModel, CommonModel> = new Map()) {
if (mergeFrom.items === undefined) { return; }
if (Array.isArray(mergeFrom.items) && mergeFrom.items.length === 0) { return; }
if (mergeTo.items === undefined) {
mergeTo.items = mergeFrom.items;
return;
}
const mergeToItems = mergeTo.items;

//mergeFrom and mergeTo is not tuple
if (!Array.isArray(mergeFrom.items) && !Array.isArray(mergeToItems)) {
mergeTo.items = CommonModel.mergeCommonModels(mergeToItems, mergeFrom.items, originalSchema, alreadyIteratedModels);
}

//mergeFrom and mergeTo is tuple
if (Array.isArray(mergeFrom.items) && Array.isArray(mergeToItems)) {
for (const [index, mergeFromTupleModel] of mergeFrom.items.entries()) {
(mergeTo.items as CommonModel[])[Number(index)] = CommonModel.mergeCommonModels(mergeToItems[Number(index)], mergeFromTupleModel, originalSchema, alreadyIteratedModels);
}
} else if (mergeTo.items !== undefined) {
mergeTo.items = merge(mergeTo.items);
}

//mergeFrom is a tuple && mergeTo is not, use mergeFrom items (the tuple is prioritized)
if (Array.isArray(mergeFrom.items) && !Array.isArray(mergeToItems)) {
mergeTo.items = mergeFrom.items;
}
//mergeFrom is not tuple && mergeTo is, do nothing
}

/**
Expand Down
23 changes: 16 additions & 7 deletions test/generators/typescript/TypeScriptGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ describe('TypeScriptGenerator', () => {
house_number: { type: 'number' },
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
array_type: { type: 'array', items: { type: 'string' } },
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
};
Expand All @@ -57,7 +58,8 @@ describe('TypeScriptGenerator', () => {
private _houseNumber: number;
private _marriage?: boolean;
private _members?: string | number | boolean;
private _arrayType: Array<string | number>;
private _tupleType?: [string, number];
private _arrayType: Array<string>;
constructor(input: {
streetName: string,
Expand All @@ -66,14 +68,16 @@ describe('TypeScriptGenerator', () => {
houseNumber: number,
marriage?: boolean,
members?: string | number | boolean,
arrayType: Array<string | number>,
tupleType?: [string, number],
arrayType: Array<string>,
}) {
this._streetName = input.streetName;
this._city = input.city;
this._state = input.state;
this._houseNumber = input.houseNumber;
this._marriage = input.marriage;
this._members = input.members;
this._tupleType = input.tupleType;
this._arrayType = input.arrayType;
}
Expand All @@ -95,8 +99,11 @@ describe('TypeScriptGenerator', () => {
get members(): string | number | boolean | undefined { return this._members; }
set members(members: string | number | boolean | undefined) { this._members = members; }
get arrayType(): Array<string | number> { return this._arrayType; }
set arrayType(arrayType: Array<string | number>) { this._arrayType = arrayType; }
get tupleType(): [string, number] | undefined { return this._tupleType; }
set tupleType(tupleType: [string, number] | undefined) { this._tupleType = tupleType; }
get arrayType(): Array<string> { return this._arrayType; }
set arrayType(arrayType: Array<string>) { this._arrayType = arrayType; }
}`;

const inputModel = await generator.process(doc);
Expand Down Expand Up @@ -160,7 +167,8 @@ ${content}`;
house_number: { type: 'number' },
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
tuple_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
array_type: { type: 'array', items: { type: 'string' } },
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
};
Expand All @@ -171,7 +179,8 @@ ${content}`;
houseNumber: number;
marriage?: boolean;
members?: string | number | boolean;
arrayType: Array<string | number>;
tupleType?: [string, number];
arrayType: Array<string>;
}`;

const interfaceGenerator = new TypeScriptGenerator({modelType: 'interface'});
Expand Down
15 changes: 15 additions & 0 deletions test/generators/typescript/TypeScriptRenderer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ describe('TypeScriptRenderer', () => {
expect(renderer.toTsType('integer', new CommonModel())).toEqual('number');
expect(renderer.toTsType('number', new CommonModel())).toEqual('number');
});
test('Should render array type', () => {
const model = new CommonModel();
model.items = CommonModel.toCommonModel({type: 'number'});
expect(renderer.toTsType('array', model)).toEqual('Array<number>');
});
test('Should render tuple type', () => {
const model = new CommonModel();
model.items = [CommonModel.toCommonModel({type: 'number'})];
expect(renderer.toTsType('array', model)).toEqual('[number]');
});
test('Should render multiple tuples', () => {
const model = new CommonModel();
model.items = [CommonModel.toCommonModel({type: 'number'}), CommonModel.toCommonModel({type: 'string'})];
expect(renderer.toTsType('array', model)).toEqual('[number, string]');
});
});
describe('renderType()', () => {
test('Should render refs with pascal case', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/interpreter/unit/InterpretItems.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ describe('Interpretation of', () => {
interpretItems(schema, model, interpreter);
expect(interpreter.interpret).toHaveBeenNthCalledWith(1, { type: 'string' }, Interpreter.defaultInterpreterOptions);
expect(interpreter.interpret).toHaveBeenNthCalledWith(2, { type: 'number' }, Interpreter.defaultInterpreterOptions);
expect(model.addItem).toHaveBeenNthCalledWith(1, mockedReturnModels[0], schema);
expect(model.addItemTuple).toHaveBeenNthCalledWith(1, mockedReturnModels[0], schema, 0);
expect(model.addItemTuple).toHaveBeenNthCalledWith(2, mockedReturnModels[0], schema, 1);
});
test('should infer type of model', () => {
const schema: any = { items: [{ type: 'string' }, { type: 'number' }] };
Expand Down

0 comments on commit aad46ef

Please sign in to comment.