From 7d3d8f55b49d868a1000c13cda24c7dbb9ea753c Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Tue, 27 Sep 2022 14:40:18 +0200 Subject: [PATCH 1/5] feat: adds support for oneOf as UnionModel --- .../__snapshots__/index.spec.ts.snap | 18 +++ .../__snapshots__/index.spec.ts.snap | 12 +- src/helpers/CommonModelToMetaModel.ts | 12 +- src/interpreter/InterpretOneOf.ts | 21 +++ src/interpreter/Interpreter.ts | 3 +- src/models/CommonModel.ts | 14 ++ .../PythonGenerator.spec.ts.snap | 131 ++++++++++++++++++ .../__snapshots__/RustGenerator.spec.ts.snap | 24 ++-- .../__snapshots__/CommonPreset.spec.ts.snap | 60 ++++---- test/helpers/CommonModelToMetaModel.spec.ts | 13 ++ test/interpreter/unit/InterpretOneOf.spec.ts | 47 +++++++ 11 files changed, 305 insertions(+), 50 deletions(-) create mode 100644 examples/generate-python-models/__snapshots__/index.spec.ts.snap create mode 100644 src/interpreter/InterpretOneOf.ts create mode 100644 test/interpreter/unit/InterpretOneOf.spec.ts diff --git a/examples/generate-python-models/__snapshots__/index.spec.ts.snap b/examples/generate-python-models/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..cedc00dda2 --- /dev/null +++ b/examples/generate-python-models/__snapshots__/index.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render python models and should log expected output to console 1`] = ` +Array [ + "class Root: + def __init__(self, input): + if hasattr(input, 'email'): + self._email = input.email + + @property + def email(self): + return self._email + @email.setter + def email(self, email): + self._email = email +", +] +`; diff --git a/examples/rust-generate-crate/__snapshots__/index.spec.ts.snap b/examples/rust-generate-crate/__snapshots__/index.spec.ts.snap index a9bfbbdc9f..5d2b82f5dc 100644 --- a/examples/rust-generate-crate/__snapshots__/index.spec.ts.snap +++ b/examples/rust-generate-crate/__snapshots__/index.spec.ts.snap @@ -52,12 +52,12 @@ Array [ "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } ", diff --git a/src/helpers/CommonModelToMetaModel.ts b/src/helpers/CommonModelToMetaModel.ts index c4d7e65d26..c37351b89d 100644 --- a/src/helpers/CommonModelToMetaModel.ts +++ b/src/helpers/CommonModelToMetaModel.ts @@ -53,11 +53,21 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode return new AnyModel(modelName, jsonSchemaModel.originalInput); } export function convertToUnionModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): UnionModel | undefined { + const unionModel = new UnionModel(name, jsonSchemaModel.originalInput, []); + + if (Array.isArray(jsonSchemaModel.union)) { + alreadySeenModels.set(jsonSchemaModel, unionModel); + for (const unionCommonModel of jsonSchemaModel.union) { + const unionMetaModel = convertToMetaModel(unionCommonModel, alreadySeenModels); + unionModel.union.push(unionMetaModel); + } + return unionModel; + } + if (!Array.isArray(jsonSchemaModel.type) || jsonSchemaModel.type.length <= 1) { return undefined; } // Has multiple types, so convert to union - const unionModel = new UnionModel(name, jsonSchemaModel.originalInput, []); alreadySeenModels.set(jsonSchemaModel, unionModel); const enumModel = convertToEnumModel(jsonSchemaModel, name); if (enumModel !== undefined) { diff --git a/src/interpreter/InterpretOneOf.ts b/src/interpreter/InterpretOneOf.ts new file mode 100644 index 0000000000..8ec73b31e1 --- /dev/null +++ b/src/interpreter/InterpretOneOf.ts @@ -0,0 +1,21 @@ +import { CommonModel } from '../models/CommonModel'; +import { Interpreter, InterpreterOptions, InterpreterSchemaType } from './Interpreter'; + +/** + * Interpreter function for oneOf keyword. + * + * It puts the schema reference into the items field. + * + * @param schema + * @param model + * @param interpreter + * @param interpreterOptions to control the interpret process + */ +export default function interpretOneOf(schema: InterpreterSchemaType, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void { + if (typeof schema === 'boolean' || schema.oneOf === undefined) { return; } + for (const oneOfSchema of schema.oneOf) { + const oneOfModel = interpreter.interpret(oneOfSchema, interpreterOptions); + if (oneOfModel === undefined) { continue; } + model.addItemUnion(oneOfModel); + } +} diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index de5d5de66b..c4be0ee3f6 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -10,6 +10,7 @@ import interpretPatternProperties from './InterpretPatternProperties'; import interpretNot from './InterpretNot'; import interpretDependencies from './InterpretDependencies'; import interpretAdditionalItems from './InterpretAdditionalItems'; +import interpretOneOf from './InterpretOneOf'; export type InterpreterOptions = { allowInheritance?: boolean @@ -78,11 +79,11 @@ export class Interpreter { interpretItems(schema, model, this, interpreterOptions); interpretProperties(schema, model, this, interpreterOptions); interpretAllOf(schema, model, this, interpreterOptions); + interpretOneOf(schema, model, this, interpreterOptions); interpretDependencies(schema, model, this, interpreterOptions); interpretConst(schema, model); interpretEnum(schema, model); - this.interpretAndCombineMultipleSchemas(schema.oneOf, model, schema, interpreterOptions); this.interpretAndCombineMultipleSchemas(schema.anyOf, model, schema, interpreterOptions); if (!(schema instanceof Draft4Schema) && !(schema instanceof Draft6Schema)) { this.interpretAndCombineSchema(schema.then, model, schema, interpreterOptions); diff --git a/src/models/CommonModel.ts b/src/models/CommonModel.ts index 56282fa50f..4d2a735ffc 100644 --- a/src/models/CommonModel.ts +++ b/src/models/CommonModel.ts @@ -15,6 +15,7 @@ export class CommonModel { $ref?: string; required?: string[]; additionalItems?: CommonModel; + union?: CommonModel[] /** * Takes a deep copy of the input object and converts it to an instance of CommonModel. @@ -189,6 +190,19 @@ export class CommonModel { this.items = modelItems; } + /** + * Adds a union model to the model. + * + * @param unionModel + */ + addItemUnion(unionModel: CommonModel): void { + if (Array.isArray(this.union)) { + this.union.push(unionModel); + } else { + this.union = [unionModel]; + } + } + /** * Add enum value to the model. * diff --git a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap index fe89d9fe92..7c7093ea8d 100644 --- a/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap +++ b/test/generators/python/__snapshots__/PythonGenerator.spec.ts.snap @@ -23,3 +23,134 @@ exports[`PythonGenerator Class should not render reserved keyword 1`] = ` self._reservedDel = reservedDel " `; + +exports[`PythonGenerator Class should render \`class\` type 1`] = ` +"class Address: + def __init__(self, input): + self._streetName = input.streetName + self._city = input.city + self._state = input.state + self._houseNumber = input.houseNumber + if hasattr(input, 'marriage'): + self._marriage = input.marriage + if hasattr(input, 'members'): + self._members = input.members + self._arrayType = input.arrayType + if hasattr(input, 'additionalProperties'): + self._additionalProperties = input.additionalProperties + + @property + def streetName(self): + return self._streetName + @streetName.setter + def streetName(self, streetName): + self._streetName = streetName + + @property + def city(self): + return self._city + @city.setter + def city(self, city): + self._city = city + + @property + def state(self): + return self._state + @state.setter + def state(self, state): + self._state = state + + @property + def houseNumber(self): + return self._houseNumber + @houseNumber.setter + def houseNumber(self, houseNumber): + self._houseNumber = houseNumber + + @property + def marriage(self): + return self._marriage + @marriage.setter + def marriage(self, marriage): + self._marriage = marriage + + @property + def members(self): + return self._members + @members.setter + def members(self, members): + self._members = members + + @property + def arrayType(self): + return self._arrayType + @arrayType.setter + def arrayType(self, arrayType): + self._arrayType = arrayType + + @property + def additionalProperties(self): + return self._additionalProperties + @additionalProperties.setter + def additionalProperties(self, additionalProperties): + self._additionalProperties = additionalProperties +" +`; + +exports[`PythonGenerator Class should work with custom preset for \`class\` type 1`] = ` +"class CustomClass: + test1 + + test1 + + + def __init__(self, input): + if hasattr(input, 'property'): + self._property = input.property + if hasattr(input, 'additionalProperties'): + self._additionalProperties = input.additionalProperties + + test2 + @property + def property(self): + return self._property + test3 + @property.setter + def property(self, property): + self._property = property + + test2 + @property + def additionalProperties(self): + return self._additionalProperties + test3 + @additionalProperties.setter + def additionalProperties(self, additionalProperties): + self._additionalProperties = additionalProperties +" +`; + +exports[`PythonGenerator Enum should render \`enum\` with mixed types (union type) 1`] = ` +"class Things(Enum): + TEXAS = \\"Texas\\" + NUMBER_1 = 1 + RESERVED_NUMBER_1 = \\"1\\" + RESERVED_FALSE = \\"false\\" + CURLYLEFT_QUOTATION_TEST_QUOTATION_COLON_QUOTATION_TEST_QUOTATION_CURLYRIGHT = \\"{\\\\\\"test\\\\\\":\\\\\\"test\\\\\\"}\\"" +`; + +exports[`PythonGenerator Enum should render enums with translated special characters 1`] = ` +"class States(Enum): + TEST_PLUS = \\"test+\\" + DOLLAR_TEST = \\"$test\\" + TEST_MINUS = \\"test-\\" + TEST_QUESTION_EXCLAMATION = \\"test?!\\" + ASTERISK_TEST = \\"*test\\"" +`; + +exports[`PythonGenerator Enum should work custom preset for \`enum\` type 1`] = ` +"class CustomEnum(Enum): + TEXAS = \\"Texas\\" + ALABAMA = \\"Alabama\\" + CALIFORNIA = \\"California\\"" +`; diff --git a/test/generators/rust/__snapshots__/RustGenerator.spec.ts.snap b/test/generators/rust/__snapshots__/RustGenerator.spec.ts.snap index 2fbf56e13e..1c26b139d2 100644 --- a/test/generators/rust/__snapshots__/RustGenerator.spec.ts.snap +++ b/test/generators/rust/__snapshots__/RustGenerator.spec.ts.snap @@ -151,12 +151,12 @@ exports[`RustGenerator Struct & Complete Models Should render complete models 2` "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } " @@ -200,12 +200,12 @@ exports[`RustGenerator Struct & Complete Models should render \`struct\` type 2 "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } " diff --git a/test/generators/rust/presets/__snapshots__/CommonPreset.spec.ts.snap b/test/generators/rust/presets/__snapshots__/CommonPreset.spec.ts.snap index 79bc61d897..41d77dece3 100644 --- a/test/generators/rust/presets/__snapshots__/CommonPreset.spec.ts.snap +++ b/test/generators/rust/presets/__snapshots__/CommonPreset.spec.ts.snap @@ -28,12 +28,12 @@ exports[`RUST_COMMON_PRESET Enum should render \`enum\` of union type with Defau "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } " @@ -43,12 +43,12 @@ exports[`RUST_COMMON_PRESET Enum should render \`enum\` of union type with Defau "// OptionalMembers represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum OptionalMembers { - #[serde(rename=\\"OptionalMembers0\\")] - OptionalMembers0(String), - #[serde(rename=\\"OptionalMembers1\\")] - OptionalMembers1(f64), - #[serde(rename=\\"OptionalMembers2\\")] - OptionalMembers2(bool), + #[serde(rename=\\"OptionalMembersOneOf_00\\")] + OptionalMembersOneOf_00(String), + #[serde(rename=\\"OptionalMembersOneOf_11\\")] + OptionalMembersOneOf_11(f64), + #[serde(rename=\\"OptionalMembersOneOf_22\\")] + OptionalMembersOneOf_22(bool), } " @@ -82,12 +82,12 @@ exports[`RUST_COMMON_PRESET Enum should render \`enum\` of union type with Defau "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } " @@ -97,12 +97,12 @@ exports[`RUST_COMMON_PRESET Enum should render \`enum\` of union type with Defau "// OptionalMembers represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum OptionalMembers { - #[serde(rename=\\"OptionalMembers0\\")] - OptionalMembers0(String), - #[serde(rename=\\"OptionalMembers1\\")] - OptionalMembers1(f64), - #[serde(rename=\\"OptionalMembers2\\")] - OptionalMembers2(bool), + #[serde(rename=\\"OptionalMembersOneOf_00\\")] + OptionalMembersOneOf_00(String), + #[serde(rename=\\"OptionalMembersOneOf_11\\")] + OptionalMembersOneOf_11(f64), + #[serde(rename=\\"OptionalMembersOneOf_22\\")] + OptionalMembersOneOf_22(bool), } " @@ -234,12 +234,12 @@ exports[`RUST_COMMON_PRESET Struct & Complete Models should render \`struct\` ty "// Members represents a union of types: String, f64, bool #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub enum Members { - #[serde(rename=\\"Members0\\")] - Members0(String), - #[serde(rename=\\"Members1\\")] - Members1(f64), - #[serde(rename=\\"Members2\\")] - Members2(bool), + #[serde(rename=\\"MembersOneOf_00\\")] + MembersOneOf_00(String), + #[serde(rename=\\"MembersOneOf_11\\")] + MembersOneOf_11(f64), + #[serde(rename=\\"MembersOneOf_22\\")] + MembersOneOf_22(bool), } " diff --git a/test/helpers/CommonModelToMetaModel.spec.ts b/test/helpers/CommonModelToMetaModel.spec.ts index 5c67739117..590a8560cf 100644 --- a/test/helpers/CommonModelToMetaModel.spec.ts +++ b/test/helpers/CommonModelToMetaModel.spec.ts @@ -210,6 +210,19 @@ describe('CommonModelToMetaModel', () => { expect(model instanceof UnionModel).toEqual(true); expect((model as UnionModel).union.length).toEqual(2); }); + test('should convert array of types to union model', () => { + const cm = new CommonModel(); + cm.$id = 'Pet'; + const cat = new CommonModel(); + cat.$id = 'Cat'; + const dog = new CommonModel(); + dog.$id = 'Dog'; + cm.union = [cat, dog]; + const model = convertToMetaModel(cm); + expect(model).not.toBeUndefined(); + expect(model instanceof UnionModel).toEqual(true); + expect((model as UnionModel).union.length).toEqual(2); + }); test('should convert tuple to tuple model', () => { const scm = new CommonModel(); scm.type = 'string'; diff --git a/test/interpreter/unit/InterpretOneOf.spec.ts b/test/interpreter/unit/InterpretOneOf.spec.ts new file mode 100644 index 0000000000..719d9a9ec9 --- /dev/null +++ b/test/interpreter/unit/InterpretOneOf.spec.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-undef */ +import { CommonModel } from '../../../src/models/CommonModel'; +import { Interpreter } from '../../../src/interpreter/Interpreter'; +import { isModelObject } from '../../../src/interpreter/Utils'; +import InterpretOneOf from '../../../src/interpreter/InterpretOneOf'; +jest.mock('../../../src/interpreter/Interpreter'); +jest.mock('../../../src/models/CommonModel'); +jest.mock('../../../src/interpreter/Utils'); +CommonModel.mergeCommonModels = jest.fn(); + +describe('Interpretation of oneOf', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('should not do anything if schema does not contain oneOf', () => { + const model = new CommonModel(); + const interpreter = new Interpreter(); + (interpreter.interpret as jest.Mock).mockReturnValue(new CommonModel()); + (isModelObject as jest.Mock).mockReturnValue(false); + + InterpretOneOf({}, model, interpreter); + + expect(interpreter.interpret).not.toHaveBeenCalled(); + expect(model.addItemUnion).not.toHaveBeenCalled(); + expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel())); + }); + + test('should add oneOf items to CommonModel union', () => { + const model = new CommonModel(); + model.addItemUnion = jest.fn(); + const schema = { oneOf: [{}, {}] }; + const interpreter = new Interpreter(); + (interpreter.interpret as jest.Mock).mockReturnValue(new CommonModel()); + (isModelObject as jest.Mock).mockReturnValue(false); + + InterpretOneOf(schema, model, interpreter, { allowInheritance: false }); + + expect(interpreter.interpret).toHaveBeenNthCalledWith(1, schema.oneOf[0], { allowInheritance: false }); + expect(interpreter.interpret).toHaveBeenNthCalledWith(2, schema.oneOf[1], { allowInheritance: false }); + expect(model.addItemUnion).toHaveBeenCalledTimes(2); + expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel())); + }); +}); From c4063cc6308923ce29f3170192b861a5d3e4570b Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Tue, 27 Sep 2022 15:37:02 +0200 Subject: [PATCH 2/5] feat: adds support for oneOf as UnionModel --- .../__snapshots__/index.spec.ts.snap | 2 +- .../__snapshots__/index.spec.ts.snap | 2 +- src/processors/AsyncAPIInputProcessor.ts | 39 ++--- .../typescript/TypeScriptGenerator.spec.ts | 130 +++++++++++++++++ .../TypeScriptGenerator.spec.ts.snap | 133 ++++++++++++++++++ .../JsonBinPackPreset.spec.ts.snap | 14 +- 6 files changed, 292 insertions(+), 28 deletions(-) diff --git a/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap b/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap index d50053894c..0a6ec88ef9 100644 --- a/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap +++ b/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Should be able to process a pure AsyncAPI object and should log expected output to console 1`] = ` Array [ - "class AnonymousSchema_1 { + "class LessAnonymousMinusMessageMinus_1Greater { private _email?: string; constructor(input: { diff --git a/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap b/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap index a7c5e5176a..a4b04661b4 100644 --- a/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap +++ b/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Should be able to process AsyncAPI object from parser and should log expected output to console 1`] = ` Array [ - "class AnonymousSchema_1 { + "class LessAnonymousMinusMessageMinus_1Greater { private _email?: string; constructor(input: { diff --git a/src/processors/AsyncAPIInputProcessor.ts b/src/processors/AsyncAPIInputProcessor.ts index 5025416a63..fe13d83aef 100644 --- a/src/processors/AsyncAPIInputProcessor.ts +++ b/src/processors/AsyncAPIInputProcessor.ts @@ -30,8 +30,8 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { } inputModel.originalInput = doc; // Go over all the message payloads and convert them to models - for (const [, message] of doc.allMessages()) { - const schema = AsyncAPIInputProcessor.convertToInternalSchema(message.payload()); + for (const [messageName, message] of doc.allMessages()) { + const schema = AsyncAPIInputProcessor.convertToInternalSchema(message.payload(), messageName); const newCommonModel = JsonSchemaInputProcessor.convertSchemaToCommonModel(schema); if (newCommonModel.$id !== undefined) { if (inputModel.models[newCommonModel.$id] !== undefined) { @@ -57,6 +57,7 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { // eslint-disable-next-line sonarjs/cognitive-complexity static convertToInternalSchema( schema: AsyncAPISchema | boolean, + name?: string, alreadyIteratedSchemas: Map = new Map() ): AsyncapiV2Schema | boolean { if (typeof schema === 'boolean') {return schema;} @@ -64,7 +65,7 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { let schemaUid = schema.uid(); //Because the constraint functionality of generators cannot handle -, <, >, we remove them from the id if it's an anonymous schema. if (schemaUid.includes('', ''); + schemaUid = name || schemaUid.replace('<', '').replace(/-/g, '_').replace('>', ''); } if (alreadyIteratedSchemas.has(schemaUid)) { @@ -76,56 +77,56 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { alreadyIteratedSchemas.set(schemaUid, convertedSchema); if (schema.allOf() !== null) { - convertedSchema.allOf = schema.allOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); + convertedSchema.allOf = schema.allOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); } if (schema.oneOf() !== null) { - convertedSchema.oneOf = schema.oneOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); + convertedSchema.oneOf = schema.oneOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); } if (schema.anyOf() !== null) { - convertedSchema.anyOf = schema.anyOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); + convertedSchema.anyOf = schema.anyOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); } if (schema.not() !== null) { - convertedSchema.not = this.convertToInternalSchema(schema.not(), alreadyIteratedSchemas); + convertedSchema.not = this.convertToInternalSchema(schema.not(), undefined, alreadyIteratedSchemas); } if ( typeof schema.additionalItems() === 'object' && schema.additionalItems() !== null ) { - convertedSchema.additionalItems = this.convertToInternalSchema(schema.additionalItems(), alreadyIteratedSchemas); + convertedSchema.additionalItems = this.convertToInternalSchema(schema.additionalItems(), undefined, alreadyIteratedSchemas); } if (schema.contains() !== null) { - convertedSchema.contains = this.convertToInternalSchema(schema.contains(), alreadyIteratedSchemas); + convertedSchema.contains = this.convertToInternalSchema(schema.contains(), undefined, alreadyIteratedSchemas); } if (schema.propertyNames() !== null) { - convertedSchema.propertyNames = this.convertToInternalSchema(schema.propertyNames(), alreadyIteratedSchemas); + convertedSchema.propertyNames = this.convertToInternalSchema(schema.propertyNames(), undefined, alreadyIteratedSchemas); } if (schema.if() !== null) { - convertedSchema.if = this.convertToInternalSchema(schema.if(), alreadyIteratedSchemas); + convertedSchema.if = this.convertToInternalSchema(schema.if(), undefined, alreadyIteratedSchemas); } if (schema.then() !== null) { - convertedSchema.then = this.convertToInternalSchema(schema.then(), alreadyIteratedSchemas); + convertedSchema.then = this.convertToInternalSchema(schema.then(), undefined, alreadyIteratedSchemas); } if (schema.else() !== null) { - convertedSchema.else = this.convertToInternalSchema(schema.else(), alreadyIteratedSchemas); + convertedSchema.else = this.convertToInternalSchema(schema.else(), undefined, alreadyIteratedSchemas); } if ( typeof schema.additionalProperties() === 'object' && schema.additionalProperties() !== null ) { - convertedSchema.additionalProperties = this.convertToInternalSchema(schema.additionalProperties(), alreadyIteratedSchemas); + convertedSchema.additionalProperties = this.convertToInternalSchema(schema.additionalProperties(), undefined, alreadyIteratedSchemas); } if (schema.items() !== null) { if (Array.isArray(schema.items())) { convertedSchema.items = (schema.items() as AsyncAPISchema[]).map((item) => this.convertToInternalSchema(item), alreadyIteratedSchemas); } else { - convertedSchema.items = this.convertToInternalSchema(schema.items() as AsyncAPISchema, alreadyIteratedSchemas); + convertedSchema.items = this.convertToInternalSchema(schema.items() as AsyncAPISchema, undefined, alreadyIteratedSchemas); } } if (schema.properties() !== null && Object.keys(schema.properties()).length) { const properties : {[key: string]: AsyncapiV2Schema | boolean} = {}; for (const [propertyName, propertySchema] of Object.entries(schema.properties())) { - properties[String(propertyName)] = this.convertToInternalSchema(propertySchema, alreadyIteratedSchemas); + properties[String(propertyName)] = this.convertToInternalSchema(propertySchema, undefined, alreadyIteratedSchemas); } convertedSchema.properties = properties; } @@ -133,7 +134,7 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { const dependencies: { [key: string]: AsyncapiV2Schema | boolean | string[] } = {}; for (const [dependencyName, dependency] of Object.entries(schema.dependencies())) { if (typeof dependency === 'object' && !Array.isArray(dependency)) { - dependencies[String(dependencyName)] = this.convertToInternalSchema(dependency, alreadyIteratedSchemas); + dependencies[String(dependencyName)] = this.convertToInternalSchema(dependency, undefined, alreadyIteratedSchemas); } else { dependencies[String(dependencyName)] = dependency as string[]; } @@ -143,14 +144,14 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { if (schema.patternProperties() !== null && Object.keys(schema.patternProperties()).length) { const patternProperties: { [key: string]: AsyncapiV2Schema | boolean } = {}; for (const [patternPropertyName, patternProperty] of Object.entries(schema.patternProperties())) { - patternProperties[String(patternPropertyName)] = this.convertToInternalSchema(patternProperty, alreadyIteratedSchemas); + patternProperties[String(patternPropertyName)] = this.convertToInternalSchema(patternProperty, undefined, alreadyIteratedSchemas); } convertedSchema.patternProperties = patternProperties; } if (schema.definitions() !== null && Object.keys(schema.definitions()).length) { const definitions: { [key: string]: AsyncapiV2Schema | boolean } = {}; for (const [definitionName, definition] of Object.entries(schema.definitions())) { - definitions[String(definitionName)] = this.convertToInternalSchema(definition, alreadyIteratedSchemas); + definitions[String(definitionName)] = this.convertToInternalSchema(definition, undefined, alreadyIteratedSchemas); } convertedSchema.definitions = definitions; } diff --git a/test/generators/typescript/TypeScriptGenerator.spec.ts b/test/generators/typescript/TypeScriptGenerator.spec.ts index bb9760f4fa..15b737e793 100644 --- a/test/generators/typescript/TypeScriptGenerator.spec.ts +++ b/test/generators/typescript/TypeScriptGenerator.spec.ts @@ -350,4 +350,134 @@ ${content}`; expect(models[0].result).toMatchSnapshot(); expect(models[1].result).toMatchSnapshot(); }); + + describe('AsyncAPI with polymorphism', () => { + const asyncapiDoc = { + asyncapi: '2.4.0', + info: { + title: 'Pet', + version: '1.0.0' + }, + channels: {}, + components: { + messages: { + PetMessage: { + payload: { + oneOf: [ + { $ref: '#/components/schemas/Cat' }, + { $ref: '#/components/schemas/Dog' }, + { $ref: '#/components/schemas/StickInsect' }, + ] + } + }, + }, + schemas: { + Pet: { + type: 'object', + additionalProperties: false, + discriminator: 'petType', + properties: { + petType: { + $id: 'PetType', + type: 'string' + }, + name: { + type: 'string' + }, + }, + required: [ + 'petType', + 'name', + ], + }, + Cat: { + allOf: [ + { $ref: '#/components/schemas/Pet' }, + { + type: 'object', + additionalProperties: false, + properties: { + petType: { + const: 'Cat' + }, + huntingSkill: { + type: 'string', + enum: [ + 'clueless', + 'lazy', + 'adventurous', + 'aggressive' + ] + } + }, + required: [ + 'huntingSkill' + ] + } + ] + }, + Dog: { + allOf: [ + { $ref: '#/components/schemas/Pet' }, + { + type: 'object', + additionalProperties: false, + properties: { + petType: { + const: 'Dog' + }, + packSize: { + type: 'integer', + format: 'int32', + description: 'the size of the pack the dog is from', + minimum: 0 + } + }, + required: [ + 'packSize' + ] + } + ] + }, + StickInsect: { + allOf: [ + { $ref: '#/components/schemas/Pet' }, + { + type: 'object', + additionalProperties: false, + properties: { + petType: { + const: 'StickBug' + }, + color: { + type: 'string', + } + }, + required: [ + 'color' + ] + } + ] + }, + } + } + }; + + test('should render 6 models (1 oneOf, 3 classes and 2 enums)', async () => { + const models = await generator.generate(asyncapiDoc); + expect(models).toHaveLength(6); + expect(models.map((model) => model.result)).toMatchSnapshot(); + }); + + test('should render enum with discriminator', async () => { + const models = await generator.generate(asyncapiDoc); + const enums = models.filter(model => model.result.includes('enum')); + + expect(enums).toHaveLength(2); + const discriminatorEnum = enums.at(0); + expect(discriminatorEnum?.modelName).not.toContain('AnonymousSchema'); + // Should contain Cat, Dog, and StickBug + expect(discriminatorEnum?.result).toMatchSnapshot(); + }); + }); }); diff --git a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap index 251305daaf..f8db95c869 100644 --- a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap +++ b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap @@ -1,5 +1,138 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`TypeScriptGenerator AsyncAPI with polymorphism should render 6 models (1 oneOf, 3 classes and 2 enums) 1`] = ` +Array [ + "type PetMessage = Cat | Dog | StickInsect;", + "class Cat { + private _petType: PetType; + private _reservedName: string; + private _huntingSkill: AnonymousSchema_5; + private _packSize?: integer; + private _color?: string; + + constructor(input: { + petType: PetType, + reservedName: string, + huntingSkill: AnonymousSchema_5, + packSize?: integer, + color?: string, + }) { + this._petType = input.petType; + this._reservedName = input.reservedName; + this._huntingSkill = input.huntingSkill; + this._packSize = input.packSize; + this._color = input.color; + } + + get petType(): PetType { return this._petType; } + set petType(petType: PetType) { this._petType = petType; } + + get reservedName(): string { return this._reservedName; } + set reservedName(reservedName: string) { this._reservedName = reservedName; } + + get huntingSkill(): AnonymousSchema_5 { return this._huntingSkill; } + set huntingSkill(huntingSkill: AnonymousSchema_5) { this._huntingSkill = huntingSkill; } + + get packSize(): integer | undefined { return this._packSize; } + set packSize(packSize: integer | undefined) { this._packSize = packSize; } + + get color(): string | undefined { return this._color; } + set color(color: string | undefined) { this._color = color; } +}", + "enum PetType { + CAT = \\"Cat\\", + DOG = \\"Dog\\", + STICK_BUG = \\"StickBug\\", +}", + "enum AnonymousSchema_5 { + CLUELESS = \\"clueless\\", + LAZY = \\"lazy\\", + ADVENTUROUS = \\"adventurous\\", + AGGRESSIVE = \\"aggressive\\", +}", + "class Dog { + private _petType: PetType; + private _reservedName: string; + private _huntingSkill?: AnonymousSchema_5; + private _packSize: integer; + private _color?: string; + + constructor(input: { + petType: PetType, + reservedName: string, + huntingSkill?: AnonymousSchema_5, + packSize: integer, + color?: string, + }) { + this._petType = input.petType; + this._reservedName = input.reservedName; + this._huntingSkill = input.huntingSkill; + this._packSize = input.packSize; + this._color = input.color; + } + + get petType(): PetType { return this._petType; } + set petType(petType: PetType) { this._petType = petType; } + + get reservedName(): string { return this._reservedName; } + set reservedName(reservedName: string) { this._reservedName = reservedName; } + + get huntingSkill(): AnonymousSchema_5 | undefined { return this._huntingSkill; } + set huntingSkill(huntingSkill: AnonymousSchema_5 | undefined) { this._huntingSkill = huntingSkill; } + + get packSize(): integer { return this._packSize; } + set packSize(packSize: integer) { this._packSize = packSize; } + + get color(): string | undefined { return this._color; } + set color(color: string | undefined) { this._color = color; } +}", + "class StickInsect { + private _petType: PetType; + private _reservedName: string; + private _huntingSkill?: AnonymousSchema_5; + private _packSize?: integer; + private _color: string; + + constructor(input: { + petType: PetType, + reservedName: string, + huntingSkill?: AnonymousSchema_5, + packSize?: integer, + color: string, + }) { + this._petType = input.petType; + this._reservedName = input.reservedName; + this._huntingSkill = input.huntingSkill; + this._packSize = input.packSize; + this._color = input.color; + } + + get petType(): PetType { return this._petType; } + set petType(petType: PetType) { this._petType = petType; } + + get reservedName(): string { return this._reservedName; } + set reservedName(reservedName: string) { this._reservedName = reservedName; } + + get huntingSkill(): AnonymousSchema_5 | undefined { return this._huntingSkill; } + set huntingSkill(huntingSkill: AnonymousSchema_5 | undefined) { this._huntingSkill = huntingSkill; } + + get packSize(): integer | undefined { return this._packSize; } + set packSize(packSize: integer | undefined) { this._packSize = packSize; } + + get color(): string { return this._color; } + set color(color: string) { this._color = color; } +}", +] +`; + +exports[`TypeScriptGenerator AsyncAPI with polymorphism should render enum with discriminator 1`] = ` +"enum PetType { + CAT = \\"Cat\\", + DOG = \\"Dog\\", + STICK_BUG = \\"StickBug\\", +}" +`; + exports[`TypeScriptGenerator should not render \`class\` with reserved keyword 1`] = ` "class Address { private _reservedReservedEnum?: string; diff --git a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap index 8e23a3f4c3..aa0e273599 100644 --- a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` -"class AnonymousSchema_1 { +"class LessAnonymousMinusMessageMinus_1Greater { private _email?: string; private _additionalProperties?: Map; @@ -36,9 +36,9 @@ exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; } - public static unmarshal(json: string | object): AnonymousSchema_1 { + public static unmarshal(json: string | object): LessAnonymousMinusMessageMinus_1Greater { const obj = typeof json === \\"object\\" ? json : JSON.parse(json); - const instance = new AnonymousSchema_1({} as any); + const instance = new LessAnonymousMinusMessageMinus_1Greater({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; @@ -54,14 +54,14 @@ exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); - const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_1\\"}); + const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"\\"}); return jsonbinpack.serialize(jsonbinpackEncodedSchema, jsonData); } - public static async jsonbinDeserialize(buffer: Buffer): Promise { - const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_1\\"}); + public static async jsonbinDeserialize(buffer: Buffer): Promise { + const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"\\"}); const json = jsonbinpack.deserialize(jsonbinpackEncodedSchema, buffer); - return AnonymousSchema_1.unmarshal(json); + return LessAnonymousMinusMessageMinus_1Greater.unmarshal(json); } }" `; From 069f3d8c1e47a57314c9acddc2ca5f98256df55d Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Tue, 27 Sep 2022 17:23:18 +0200 Subject: [PATCH 3/5] review fix --- .../__snapshots__/index.spec.ts.snap | 2 +- .../__snapshots__/index.spec.ts.snap | 2 +- src/helpers/CommonModelToMetaModel.ts | 21 +++++----- src/processors/AsyncAPIInputProcessor.ts | 39 +++++++++---------- .../TypeScriptGenerator.spec.ts.snap | 2 +- .../JsonBinPackPreset.spec.ts.snap | 14 +++---- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap b/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap index 0a6ec88ef9..d50053894c 100644 --- a/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap +++ b/examples/asyncapi-from-object/__snapshots__/index.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Should be able to process a pure AsyncAPI object and should log expected output to console 1`] = ` Array [ - "class LessAnonymousMinusMessageMinus_1Greater { + "class AnonymousSchema_1 { private _email?: string; constructor(input: { diff --git a/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap b/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap index a4b04661b4..a7c5e5176a 100644 --- a/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap +++ b/examples/asyncapi-from-parser/__snapshots__/index.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Should be able to process AsyncAPI object from parser and should log expected output to console 1`] = ` Array [ - "class LessAnonymousMinusMessageMinus_1Greater { + "class AnonymousSchema_1 { private _email?: string; constructor(input: { diff --git a/src/helpers/CommonModelToMetaModel.ts b/src/helpers/CommonModelToMetaModel.ts index c37351b89d..3e6167c7b9 100644 --- a/src/helpers/CommonModelToMetaModel.ts +++ b/src/helpers/CommonModelToMetaModel.ts @@ -53,22 +53,23 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode return new AnyModel(modelName, jsonSchemaModel.originalInput); } export function convertToUnionModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): UnionModel | undefined { + const containsUnions = Array.isArray(jsonSchemaModel.union); + const containsSimpleTypeUnion = Array.isArray(jsonSchemaModel.type) && jsonSchemaModel.type.length > 1 + if (!containsSimpleTypeUnion && !containsUnions) { + return undefined; + } const unionModel = new UnionModel(name, jsonSchemaModel.originalInput, []); - - if (Array.isArray(jsonSchemaModel.union)) { - alreadySeenModels.set(jsonSchemaModel, unionModel); + alreadySeenModels.set(jsonSchemaModel, unionModel); + + // Has multiple types, so convert to union + if (containsUnions && jsonSchemaModel.union) { for (const unionCommonModel of jsonSchemaModel.union) { const unionMetaModel = convertToMetaModel(unionCommonModel, alreadySeenModels); unionModel.union.push(unionMetaModel); } return unionModel; - } - - if (!Array.isArray(jsonSchemaModel.type) || jsonSchemaModel.type.length <= 1) { - return undefined; - } - // Has multiple types, so convert to union - alreadySeenModels.set(jsonSchemaModel, unionModel); + } + // Has simple union types const enumModel = convertToEnumModel(jsonSchemaModel, name); if (enumModel !== undefined) { unionModel.union.push(enumModel); diff --git a/src/processors/AsyncAPIInputProcessor.ts b/src/processors/AsyncAPIInputProcessor.ts index fe13d83aef..5025416a63 100644 --- a/src/processors/AsyncAPIInputProcessor.ts +++ b/src/processors/AsyncAPIInputProcessor.ts @@ -30,8 +30,8 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { } inputModel.originalInput = doc; // Go over all the message payloads and convert them to models - for (const [messageName, message] of doc.allMessages()) { - const schema = AsyncAPIInputProcessor.convertToInternalSchema(message.payload(), messageName); + for (const [, message] of doc.allMessages()) { + const schema = AsyncAPIInputProcessor.convertToInternalSchema(message.payload()); const newCommonModel = JsonSchemaInputProcessor.convertSchemaToCommonModel(schema); if (newCommonModel.$id !== undefined) { if (inputModel.models[newCommonModel.$id] !== undefined) { @@ -57,7 +57,6 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { // eslint-disable-next-line sonarjs/cognitive-complexity static convertToInternalSchema( schema: AsyncAPISchema | boolean, - name?: string, alreadyIteratedSchemas: Map = new Map() ): AsyncapiV2Schema | boolean { if (typeof schema === 'boolean') {return schema;} @@ -65,7 +64,7 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { let schemaUid = schema.uid(); //Because the constraint functionality of generators cannot handle -, <, >, we remove them from the id if it's an anonymous schema. if (schemaUid.includes('', ''); + schemaUid = schemaUid.replace('<', '').replace(/-/g, '_').replace('>', ''); } if (alreadyIteratedSchemas.has(schemaUid)) { @@ -77,56 +76,56 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { alreadyIteratedSchemas.set(schemaUid, convertedSchema); if (schema.allOf() !== null) { - convertedSchema.allOf = schema.allOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); + convertedSchema.allOf = schema.allOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); } if (schema.oneOf() !== null) { - convertedSchema.oneOf = schema.oneOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); + convertedSchema.oneOf = schema.oneOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); } if (schema.anyOf() !== null) { - convertedSchema.anyOf = schema.anyOf().map((item) => this.convertToInternalSchema(item, undefined, alreadyIteratedSchemas)); + convertedSchema.anyOf = schema.anyOf().map((item) => this.convertToInternalSchema(item, alreadyIteratedSchemas)); } if (schema.not() !== null) { - convertedSchema.not = this.convertToInternalSchema(schema.not(), undefined, alreadyIteratedSchemas); + convertedSchema.not = this.convertToInternalSchema(schema.not(), alreadyIteratedSchemas); } if ( typeof schema.additionalItems() === 'object' && schema.additionalItems() !== null ) { - convertedSchema.additionalItems = this.convertToInternalSchema(schema.additionalItems(), undefined, alreadyIteratedSchemas); + convertedSchema.additionalItems = this.convertToInternalSchema(schema.additionalItems(), alreadyIteratedSchemas); } if (schema.contains() !== null) { - convertedSchema.contains = this.convertToInternalSchema(schema.contains(), undefined, alreadyIteratedSchemas); + convertedSchema.contains = this.convertToInternalSchema(schema.contains(), alreadyIteratedSchemas); } if (schema.propertyNames() !== null) { - convertedSchema.propertyNames = this.convertToInternalSchema(schema.propertyNames(), undefined, alreadyIteratedSchemas); + convertedSchema.propertyNames = this.convertToInternalSchema(schema.propertyNames(), alreadyIteratedSchemas); } if (schema.if() !== null) { - convertedSchema.if = this.convertToInternalSchema(schema.if(), undefined, alreadyIteratedSchemas); + convertedSchema.if = this.convertToInternalSchema(schema.if(), alreadyIteratedSchemas); } if (schema.then() !== null) { - convertedSchema.then = this.convertToInternalSchema(schema.then(), undefined, alreadyIteratedSchemas); + convertedSchema.then = this.convertToInternalSchema(schema.then(), alreadyIteratedSchemas); } if (schema.else() !== null) { - convertedSchema.else = this.convertToInternalSchema(schema.else(), undefined, alreadyIteratedSchemas); + convertedSchema.else = this.convertToInternalSchema(schema.else(), alreadyIteratedSchemas); } if ( typeof schema.additionalProperties() === 'object' && schema.additionalProperties() !== null ) { - convertedSchema.additionalProperties = this.convertToInternalSchema(schema.additionalProperties(), undefined, alreadyIteratedSchemas); + convertedSchema.additionalProperties = this.convertToInternalSchema(schema.additionalProperties(), alreadyIteratedSchemas); } if (schema.items() !== null) { if (Array.isArray(schema.items())) { convertedSchema.items = (schema.items() as AsyncAPISchema[]).map((item) => this.convertToInternalSchema(item), alreadyIteratedSchemas); } else { - convertedSchema.items = this.convertToInternalSchema(schema.items() as AsyncAPISchema, undefined, alreadyIteratedSchemas); + convertedSchema.items = this.convertToInternalSchema(schema.items() as AsyncAPISchema, alreadyIteratedSchemas); } } if (schema.properties() !== null && Object.keys(schema.properties()).length) { const properties : {[key: string]: AsyncapiV2Schema | boolean} = {}; for (const [propertyName, propertySchema] of Object.entries(schema.properties())) { - properties[String(propertyName)] = this.convertToInternalSchema(propertySchema, undefined, alreadyIteratedSchemas); + properties[String(propertyName)] = this.convertToInternalSchema(propertySchema, alreadyIteratedSchemas); } convertedSchema.properties = properties; } @@ -134,7 +133,7 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { const dependencies: { [key: string]: AsyncapiV2Schema | boolean | string[] } = {}; for (const [dependencyName, dependency] of Object.entries(schema.dependencies())) { if (typeof dependency === 'object' && !Array.isArray(dependency)) { - dependencies[String(dependencyName)] = this.convertToInternalSchema(dependency, undefined, alreadyIteratedSchemas); + dependencies[String(dependencyName)] = this.convertToInternalSchema(dependency, alreadyIteratedSchemas); } else { dependencies[String(dependencyName)] = dependency as string[]; } @@ -144,14 +143,14 @@ export class AsyncAPIInputProcessor extends AbstractInputProcessor { if (schema.patternProperties() !== null && Object.keys(schema.patternProperties()).length) { const patternProperties: { [key: string]: AsyncapiV2Schema | boolean } = {}; for (const [patternPropertyName, patternProperty] of Object.entries(schema.patternProperties())) { - patternProperties[String(patternPropertyName)] = this.convertToInternalSchema(patternProperty, undefined, alreadyIteratedSchemas); + patternProperties[String(patternPropertyName)] = this.convertToInternalSchema(patternProperty, alreadyIteratedSchemas); } convertedSchema.patternProperties = patternProperties; } if (schema.definitions() !== null && Object.keys(schema.definitions()).length) { const definitions: { [key: string]: AsyncapiV2Schema | boolean } = {}; for (const [definitionName, definition] of Object.entries(schema.definitions())) { - definitions[String(definitionName)] = this.convertToInternalSchema(definition, undefined, alreadyIteratedSchemas); + definitions[String(definitionName)] = this.convertToInternalSchema(definition, alreadyIteratedSchemas); } convertedSchema.definitions = definitions; } diff --git a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap index f8db95c869..9c63c487fa 100644 --- a/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap +++ b/test/generators/typescript/__snapshots__/TypeScriptGenerator.spec.ts.snap @@ -2,7 +2,7 @@ exports[`TypeScriptGenerator AsyncAPI with polymorphism should render 6 models (1 oneOf, 3 classes and 2 enums) 1`] = ` Array [ - "type PetMessage = Cat | Dog | StickInsect;", + "type AnonymousSchema_1 = Cat | Dog | StickInsect;", "class Cat { private _petType: PetType; private _reservedName: string; diff --git a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap index aa0e273599..8e23a3f4c3 100644 --- a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` -"class LessAnonymousMinusMessageMinus_1Greater { +"class AnonymousSchema_1 { private _email?: string; private _additionalProperties?: Map; @@ -36,9 +36,9 @@ exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; } - public static unmarshal(json: string | object): LessAnonymousMinusMessageMinus_1Greater { + public static unmarshal(json: string | object): AnonymousSchema_1 { const obj = typeof json === \\"object\\" ? json : JSON.parse(json); - const instance = new LessAnonymousMinusMessageMinus_1Greater({} as any); + const instance = new AnonymousSchema_1({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; @@ -54,14 +54,14 @@ exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); - const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"\\"}); + const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_1\\"}); return jsonbinpack.serialize(jsonbinpackEncodedSchema, jsonData); } - public static async jsonbinDeserialize(buffer: Buffer): Promise { - const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"\\"}); + public static async jsonbinDeserialize(buffer: Buffer): Promise { + const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"type\\":\\"string\\",\\"format\\":\\"email\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_2\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"anonymous_schema_1\\"}); const json = jsonbinpack.deserialize(jsonbinpackEncodedSchema, buffer); - return LessAnonymousMinusMessageMinus_1Greater.unmarshal(json); + return AnonymousSchema_1.unmarshal(json); } }" `; From d422be61d4fe5596f2e12126e28cfdfd2995a3c0 Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Tue, 27 Sep 2022 17:38:21 +0200 Subject: [PATCH 4/5] review fix --- src/helpers/CommonModelToMetaModel.ts | 2 +- src/interpreter/InterpretAnyOf.ts | 21 +++++++++ src/interpreter/Interpreter.ts | 18 +------- test/interpreter/unit/Intepreter.spec.ts | 13 +----- test/interpreter/unit/InterpretAnyOf.spec.ts | 47 ++++++++++++++++++++ 5 files changed, 72 insertions(+), 29 deletions(-) create mode 100644 src/interpreter/InterpretAnyOf.ts create mode 100644 test/interpreter/unit/InterpretAnyOf.spec.ts diff --git a/src/helpers/CommonModelToMetaModel.ts b/src/helpers/CommonModelToMetaModel.ts index 3e6167c7b9..6f15865b58 100644 --- a/src/helpers/CommonModelToMetaModel.ts +++ b/src/helpers/CommonModelToMetaModel.ts @@ -54,7 +54,7 @@ export function convertToMetaModel(jsonSchemaModel: CommonModel, alreadySeenMode } export function convertToUnionModel(jsonSchemaModel: CommonModel, name: string, alreadySeenModels: Map): UnionModel | undefined { const containsUnions = Array.isArray(jsonSchemaModel.union); - const containsSimpleTypeUnion = Array.isArray(jsonSchemaModel.type) && jsonSchemaModel.type.length > 1 + const containsSimpleTypeUnion = Array.isArray(jsonSchemaModel.type) && jsonSchemaModel.type.length > 1; if (!containsSimpleTypeUnion && !containsUnions) { return undefined; } diff --git a/src/interpreter/InterpretAnyOf.ts b/src/interpreter/InterpretAnyOf.ts new file mode 100644 index 0000000000..72c83340e4 --- /dev/null +++ b/src/interpreter/InterpretAnyOf.ts @@ -0,0 +1,21 @@ +import { CommonModel } from '../models/CommonModel'; +import { Interpreter, InterpreterOptions, InterpreterSchemaType } from './Interpreter'; + +/** + * Interpreter function for anyOf keyword. + * + * It puts the schema reference into the items field. + * + * @param schema + * @param model + * @param interpreter + * @param interpreterOptions to control the interpret process + */ +export default function interpretAnyOf(schema: InterpreterSchemaType, model: CommonModel, interpreter : Interpreter, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void { + if (typeof schema === 'boolean' || schema.anyOf === undefined) { return; } + for (const anyOfSchema of schema.anyOf) { + const anyOfModel = interpreter.interpret(anyOfSchema, interpreterOptions); + if (anyOfModel === undefined) { continue; } + model.addItemUnion(anyOfModel); + } +} diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index c4be0ee3f6..e1b5a555e3 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -11,6 +11,7 @@ import interpretNot from './InterpretNot'; import interpretDependencies from './InterpretDependencies'; import interpretAdditionalItems from './InterpretAdditionalItems'; import interpretOneOf from './InterpretOneOf'; +import interpretAnyOf from './InterpretAnyOf'; export type InterpreterOptions = { allowInheritance?: boolean @@ -80,11 +81,11 @@ export class Interpreter { interpretProperties(schema, model, this, interpreterOptions); interpretAllOf(schema, model, this, interpreterOptions); interpretOneOf(schema, model, this, interpreterOptions); + interpretAnyOf(schema, model, this, interpreterOptions); interpretDependencies(schema, model, this, interpreterOptions); interpretConst(schema, model); interpretEnum(schema, model); - this.interpretAndCombineMultipleSchemas(schema.anyOf, model, schema, interpreterOptions); if (!(schema instanceof Draft4Schema) && !(schema instanceof Draft6Schema)) { this.interpretAndCombineSchema(schema.then, model, schema, interpreterOptions); this.interpretAndCombineSchema(schema.else, model, schema, interpreterOptions); @@ -111,19 +112,4 @@ export class Interpreter { CommonModel.mergeCommonModels(currentModel, model, rootSchema); } } - - /** - * Go through multiple schemas and combine the interpreted models together. - * - * @param schema to go through - * @param currentModel the current output - * @param rootSchema the root schema to use as original schema when merged - * @param interpreterOptions to control the interpret process - */ - interpretAndCombineMultipleSchemas(schema: InterpreterSchemaType[] | undefined, currentModel: CommonModel, rootSchema: any, interpreterOptions: InterpreterOptions = Interpreter.defaultInterpreterOptions): void { - if (!Array.isArray(schema)) { return; } - for (const forEachSchema of schema) { - this.interpretAndCombineSchema(forEachSchema, currentModel, rootSchema, interpreterOptions); - } - } } diff --git a/test/interpreter/unit/Intepreter.spec.ts b/test/interpreter/unit/Intepreter.spec.ts index 0861027f18..e9361118c8 100644 --- a/test/interpreter/unit/Intepreter.spec.ts +++ b/test/interpreter/unit/Intepreter.spec.ts @@ -105,7 +105,7 @@ describe('Interpreter', () => { const interpreter = new Interpreter(); const model = interpreter.interpret(schema1); expect(model).not.toBeUndefined(); - expect(model).toEqual({originalInput: schema1, $id: 'anonymSchema2'}); + expect(model).toMatchObject({originalInput: schema1, $id: 'anonymSchema2'}); }); describe('combineSchemas', () => { test('should combine single schema with model', () => { @@ -119,17 +119,6 @@ describe('Interpreter', () => { interpreter.interpretAndCombineSchema(schema, model, schema); expect(CommonModel.mergeCommonModels).toHaveBeenNthCalledWith(1, model, expectedSimplifiedModel, schema); }); - test('should combine multiple schema with model', () => { - const schema = { required: ['test'] }; - const interpreter = new Interpreter(); - const model = new CommonModel(); - const expectedSimplifiedModel = new CommonModel(); - expectedSimplifiedModel.$id = 'anonymSchema1'; - expectedSimplifiedModel.required = ['test']; - expectedSimplifiedModel.originalInput = schema; - interpreter.interpretAndCombineMultipleSchemas([schema], model, schema); - expect(CommonModel.mergeCommonModels).toHaveBeenNthCalledWith(1, model, expectedSimplifiedModel, schema); - }); }); test('should always try to interpret properties', () => { const schema = {}; diff --git a/test/interpreter/unit/InterpretAnyOf.spec.ts b/test/interpreter/unit/InterpretAnyOf.spec.ts new file mode 100644 index 0000000000..dd635523af --- /dev/null +++ b/test/interpreter/unit/InterpretAnyOf.spec.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-undef */ +import { CommonModel } from '../../../src/models/CommonModel'; +import { Interpreter } from '../../../src/interpreter/Interpreter'; +import { isModelObject } from '../../../src/interpreter/Utils'; +import InterpretAnyOf from '../../../src/interpreter/InterpretAnyOf'; +jest.mock('../../../src/interpreter/Interpreter'); +jest.mock('../../../src/models/CommonModel'); +jest.mock('../../../src/interpreter/Utils'); +CommonModel.mergeCommonModels = jest.fn(); + +describe('Interpretation of anyOf', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); + + test('should not do anything if schema does not contain anyOf', () => { + const model = new CommonModel(); + const interpreter = new Interpreter(); + (interpreter.interpret as jest.Mock).mockReturnValue(new CommonModel()); + (isModelObject as jest.Mock).mockReturnValue(false); + + InterpretAnyOf({}, model, interpreter); + + expect(interpreter.interpret).not.toHaveBeenCalled(); + expect(model.addItemUnion).not.toHaveBeenCalled(); + expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel())); + }); + + test('should add anyOf items to CommonModel union', () => { + const model = new CommonModel(); + model.addItemUnion = jest.fn(); + const schema = { anyOf: [{}, {}] }; + const interpreter = new Interpreter(); + (interpreter.interpret as jest.Mock).mockReturnValue(new CommonModel()); + (isModelObject as jest.Mock).mockReturnValue(false); + + InterpretAnyOf(schema, model, interpreter, { allowInheritance: false }); + + expect(interpreter.interpret).toHaveBeenNthCalledWith(1, schema.anyOf[0], { allowInheritance: false }); + expect(interpreter.interpret).toHaveBeenNthCalledWith(2, schema.anyOf[1], { allowInheritance: false }); + expect(model.addItemUnion).toHaveBeenCalledTimes(2); + expect(JSON.stringify(model)).toEqual(JSON.stringify(new CommonModel())); + }); +}); From 83a021835ac2fe1dd37f4ed86d28eb95e510ea13 Mon Sep 17 00:00:00 2001 From: Kenneth Aasan Date: Tue, 27 Sep 2022 17:44:27 +0200 Subject: [PATCH 5/5] review fix --- test/generators/typescript/TypeScriptGenerator.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/generators/typescript/TypeScriptGenerator.spec.ts b/test/generators/typescript/TypeScriptGenerator.spec.ts index 15b737e793..6ca9781555 100644 --- a/test/generators/typescript/TypeScriptGenerator.spec.ts +++ b/test/generators/typescript/TypeScriptGenerator.spec.ts @@ -474,7 +474,7 @@ ${content}`; const enums = models.filter(model => model.result.includes('enum')); expect(enums).toHaveLength(2); - const discriminatorEnum = enums.at(0); + const discriminatorEnum = enums[0]; expect(discriminatorEnum?.modelName).not.toContain('AnonymousSchema'); // Should contain Cat, Dog, and StickBug expect(discriminatorEnum?.result).toMatchSnapshot();