From be4041666138bdfd18a542587d28b2d0d1ddcff1 Mon Sep 17 00:00:00 2001 From: akkshitgupta Date: Sun, 21 Jan 2024 23:57:26 +0530 Subject: [PATCH 01/13] feat: add Avro Schema input processor --- src/models/AvroSchema.ts | 87 ++++++++++++ src/models/index.ts | 1 + src/processors/AvroSchemaInputProcessor.ts | 152 +++++++++++++++++++++ src/processors/InputProcessor.ts | 2 + src/processors/index.ts | 1 + 5 files changed, 243 insertions(+) create mode 100644 src/models/AvroSchema.ts create mode 100644 src/processors/AvroSchemaInputProcessor.ts diff --git a/src/models/AvroSchema.ts b/src/models/AvroSchema.ts new file mode 100644 index 0000000000..b6fc835037 --- /dev/null +++ b/src/models/AvroSchema.ts @@ -0,0 +1,87 @@ +/** + * Avro Schema model + */ + +// clear unknown to make code more robust + +export interface AvroField { + name?: string; + doc?: string; + type?: unknown; + default?: unknown; +} + +export class AvroSchema { + type?: string | string[] | object; + name?: string; + namespace?: string; + doc?: string; + aliases?: string[]; + symbols?: string[]; + items?: unknown; + values?: unknown; + fields?: AvroField[]; + order?: 'ascending' | 'descending' | 'ignore'; + size?: number; + example?: string | number; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + default?: unknown; + exclusiveMinimum?: unknown; + exclusiveMaximum?: unknown; + logicalType?: unknown; + + /** + * Takes a deep copy of the input object and converts it to an instance of AvroSchema. + * + * @param object + */ + static toSchema(object: Record): AvroSchema { + const convertedSchema = AvroSchema.internalToSchema(object); + if (convertedSchema instanceof AvroSchema) { + return convertedSchema; + } + throw new Error('Could not convert input to expected copy of AvroSchema'); + } + + private static internalToSchema( + object: any, + seenSchemas: Map = new Map() + ): any { + // if primitive types return as is + if (null === object || 'object' !== typeof object) { + return object; + } + + if (seenSchemas.has(object)) { + return seenSchemas.get(object) as any; + } + + if (object instanceof Array) { + const copy: any = []; + for (let i = 0, len = object.length; i < len; i++) { + copy[Number(i)] = AvroSchema.internalToSchema( + object[Number(i)], + seenSchemas + ); + } + return copy; + } + //Nothing else left then to create an object + const schema = new AvroSchema(); + seenSchemas.set(object, schema); + for (const [propName, prop] of Object.entries(object)) { + let copyProp = prop; + + // Ignore value properties (those with `any` type) as they should be saved as is regardless of value + if (propName !== 'default' && propName !== 'enum') { + copyProp = AvroSchema.internalToSchema(prop, seenSchemas); + } + (schema as any)[String(propName)] = copyProp; + } + return schema; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 555d5d5352..42e94f7ca2 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -12,3 +12,4 @@ export * from './OpenapiV3Schema'; export * from './MetaModel'; export * from './ConstrainedMetaModel'; export * from './InputMetaModel'; +export * from './AvroSchema'; diff --git a/src/processors/AvroSchemaInputProcessor.ts b/src/processors/AvroSchemaInputProcessor.ts new file mode 100644 index 0000000000..5319bc9b4a --- /dev/null +++ b/src/processors/AvroSchemaInputProcessor.ts @@ -0,0 +1,152 @@ +import { AbstractInputProcessor } from './AbstractInputProcessor'; +import { + CommonModel, + InputMetaModel, + AvroSchema, + ProcessorOptions +} from '../models'; +import { Logger } from '../utils'; +import { Interpreter } from '../interpreter/Interpreter'; +import { convertToMetaModel } from '../helpers'; + +/** + * Class for processing Avro Schema input + */ +export class AvroSchemaInputProcessor extends AbstractInputProcessor { + /** + * Function processing an Avro Schema input + * + * @param input + */ + + shouldProcess(input?: any): boolean { + if ( + input === '' || + JSON.stringify(input) === '{}' || + JSON.stringify(input) === '[]' + ) { + return false; + } + if (!input.type) { + return false; + } + return true; + } + + process(input?: any, options?: ProcessorOptions): Promise { + if (!this.shouldProcess(input)) { + return Promise.reject( + new Error('Input is not an Avro Schema, so it cannot be processed.') + ); + } + Logger.debug('Processing input as Avro Schema document'); + const inputModel = new InputMetaModel(); + inputModel.originalInput = input; + input = AvroSchemaInputProcessor.reflectSchemaNames( + input, + {}, + 'root', + true + ) as any; + const parsedSchema = AvroSchema.toSchema(input); + const newCommonModel = AvroSchemaInputProcessor.convertSchemaToCommonModel( + parsedSchema, + options + ); + const metaModel = convertToMetaModel(newCommonModel); + inputModel.models[metaModel.name] = metaModel; + Logger.debug('Completed processing input as Avro Schema document'); + + return Promise.resolve(inputModel); + } + + /** + * Each schema must have a name, so when later interpreted, the model have the most accurate model name. + * + * Reflect name from given schema and save it to `x-modelgen-inferred-name` extension. + * + * This reflects all the common keywords that are shared between draft-4, draft-7 and Swagger 2.0 Schema + * + * @param schema to process + * @param namesStack is a aggegator of previous used names + * @param name to infer + * @param isRoot indicates if performed schema is a root schema + */ + static reflectSchemaNames( + schema: AvroSchema | boolean, + namesStack: Record, + name?: string, + isRoot?: boolean + ): any { + if (typeof schema === 'boolean') { + return schema; + } + + schema = Object.assign({}, schema); + if (isRoot) { + namesStack[String(name)] = 0; + (schema as any)[this.MODELGEN_INFFERED_NAME] = name; + name = ''; + } else if ( + name && + !(schema as any)[this.MODELGEN_INFFERED_NAME] + // && schema.$ref === undefined + ) { + let occurrence = namesStack[String(name)]; + if (occurrence === undefined) { + namesStack[String(name)] = 0; + } else { + occurrence++; + } + const inferredName = occurrence ? `${name}_${occurrence}` : name; + (schema as any)[this.MODELGEN_INFFERED_NAME] = inferredName; + } + + if (schema.fields !== undefined) { + schema.fields = (schema.fields as any[]).map((item: any, idx: number) => + this.reflectSchemaNames( + item, + namesStack, + this.ensureNamePattern(name, 'fields', idx) + ) + ); + } + if (typeof schema.type === 'object' && schema.type !== undefined) { + schema.type = this.reflectSchemaNames( + schema.type, + namesStack, + this.ensureNamePattern(name, 'type') + ); + } + return schema; + } + + /** + * Ensure schema name using previous name and new part + * + * @param previousName to concatenate with + * @param newParts + */ + private static ensureNamePattern( + previousName: string | undefined, + ...newParts: any[] + ): string { + const pattern = newParts.map((part) => `${part}`).join('_'); + if (!previousName) { + return pattern; + } + return `${previousName}_${pattern}`; + } + + static convertSchemaToCommonModel( + schema: AvroSchema | boolean, + options?: ProcessorOptions + ): CommonModel { + const interpreter = new Interpreter(); + const model = interpreter.interpret(schema as any, options?.interpreter); + if (model === undefined) { + throw new Error('Could not interpret schema to internal model'); + } + return model; + } +} diff --git a/src/processors/InputProcessor.ts b/src/processors/InputProcessor.ts index 6dc3529007..6a63fce83c 100644 --- a/src/processors/InputProcessor.ts +++ b/src/processors/InputProcessor.ts @@ -5,6 +5,7 @@ import { ProcessorOptions, InputMetaModel } from '../models'; import { SwaggerInputProcessor } from './SwaggerInputProcessor'; import { OpenAPIInputProcessor } from './OpenAPIInputProcessor'; import { TypeScriptInputProcessor } from './TypeScriptInputProcessor'; +import { AvroSchemaInputProcessor } from './AvroSchemaInputProcessor'; /** * Main input processor which figures out the type of input it receives and delegates the processing into separate individual processors. @@ -19,6 +20,7 @@ export class InputProcessor { this.setProcessor('openapi', new OpenAPIInputProcessor()); this.setProcessor('default', new JsonSchemaInputProcessor()); this.setProcessor('typescript', new TypeScriptInputProcessor()); + this.setProcessor('avroSchema', new AvroSchemaInputProcessor()); } /** diff --git a/src/processors/index.ts b/src/processors/index.ts index 22a5d42ccc..28a7f17b5f 100644 --- a/src/processors/index.ts +++ b/src/processors/index.ts @@ -5,3 +5,4 @@ export * from './JsonSchemaInputProcessor'; export * from './SwaggerInputProcessor'; export * from './OpenAPIInputProcessor'; export * from './TypeScriptInputProcessor'; +export * from './AvroSchemaInputProcessor'; From 4c4af485d9e7c927b3a761d56a8079dd2b07ecd1 Mon Sep 17 00:00:00 2001 From: akkshitgupta Date: Tue, 23 Jan 2024 21:55:45 +0530 Subject: [PATCH 02/13] remove reflectSchemaNames method --- src/processors/AvroSchemaInputProcessor.ts | 113 +-------------------- 1 file changed, 3 insertions(+), 110 deletions(-) diff --git a/src/processors/AvroSchemaInputProcessor.ts b/src/processors/AvroSchemaInputProcessor.ts index 5319bc9b4a..937858359a 100644 --- a/src/processors/AvroSchemaInputProcessor.ts +++ b/src/processors/AvroSchemaInputProcessor.ts @@ -1,12 +1,6 @@ import { AbstractInputProcessor } from './AbstractInputProcessor'; -import { - CommonModel, - InputMetaModel, - AvroSchema, - ProcessorOptions -} from '../models'; +import { InputMetaModel } from '../models'; import { Logger } from '../utils'; -import { Interpreter } from '../interpreter/Interpreter'; import { convertToMetaModel } from '../helpers'; /** @@ -33,7 +27,7 @@ export class AvroSchemaInputProcessor extends AbstractInputProcessor { return true; } - process(input?: any, options?: ProcessorOptions): Promise { + process(input?: any): Promise { if (!this.shouldProcess(input)) { return Promise.reject( new Error('Input is not an Avro Schema, so it cannot be processed.') @@ -42,111 +36,10 @@ export class AvroSchemaInputProcessor extends AbstractInputProcessor { Logger.debug('Processing input as Avro Schema document'); const inputModel = new InputMetaModel(); inputModel.originalInput = input; - input = AvroSchemaInputProcessor.reflectSchemaNames( - input, - {}, - 'root', - true - ) as any; - const parsedSchema = AvroSchema.toSchema(input); - const newCommonModel = AvroSchemaInputProcessor.convertSchemaToCommonModel( - parsedSchema, - options - ); - const metaModel = convertToMetaModel(newCommonModel); + const metaModel = convertToMetaModel(input); inputModel.models[metaModel.name] = metaModel; Logger.debug('Completed processing input as Avro Schema document'); return Promise.resolve(inputModel); } - - /** - * Each schema must have a name, so when later interpreted, the model have the most accurate model name. - * - * Reflect name from given schema and save it to `x-modelgen-inferred-name` extension. - * - * This reflects all the common keywords that are shared between draft-4, draft-7 and Swagger 2.0 Schema - * - * @param schema to process - * @param namesStack is a aggegator of previous used names - * @param name to infer - * @param isRoot indicates if performed schema is a root schema - */ - static reflectSchemaNames( - schema: AvroSchema | boolean, - namesStack: Record, - name?: string, - isRoot?: boolean - ): any { - if (typeof schema === 'boolean') { - return schema; - } - - schema = Object.assign({}, schema); - if (isRoot) { - namesStack[String(name)] = 0; - (schema as any)[this.MODELGEN_INFFERED_NAME] = name; - name = ''; - } else if ( - name && - !(schema as any)[this.MODELGEN_INFFERED_NAME] - // && schema.$ref === undefined - ) { - let occurrence = namesStack[String(name)]; - if (occurrence === undefined) { - namesStack[String(name)] = 0; - } else { - occurrence++; - } - const inferredName = occurrence ? `${name}_${occurrence}` : name; - (schema as any)[this.MODELGEN_INFFERED_NAME] = inferredName; - } - - if (schema.fields !== undefined) { - schema.fields = (schema.fields as any[]).map((item: any, idx: number) => - this.reflectSchemaNames( - item, - namesStack, - this.ensureNamePattern(name, 'fields', idx) - ) - ); - } - if (typeof schema.type === 'object' && schema.type !== undefined) { - schema.type = this.reflectSchemaNames( - schema.type, - namesStack, - this.ensureNamePattern(name, 'type') - ); - } - return schema; - } - - /** - * Ensure schema name using previous name and new part - * - * @param previousName to concatenate with - * @param newParts - */ - private static ensureNamePattern( - previousName: string | undefined, - ...newParts: any[] - ): string { - const pattern = newParts.map((part) => `${part}`).join('_'); - if (!previousName) { - return pattern; - } - return `${previousName}_${pattern}`; - } - - static convertSchemaToCommonModel( - schema: AvroSchema | boolean, - options?: ProcessorOptions - ): CommonModel { - const interpreter = new Interpreter(); - const model = interpreter.interpret(schema as any, options?.interpreter); - if (model === undefined) { - throw new Error('Could not interpret schema to internal model'); - } - return model; - } } From a36cafe35f3b6e03f1f2a9947aace859acc1cb84 Mon Sep 17 00:00:00 2001 From: akkshitgupta Date: Wed, 24 Jan 2024 01:30:00 +0530 Subject: [PATCH 03/13] test: add test for Avro Schema input processor --- .../AvroSchemaInputProcessor.spec.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/processors/AvroSchemaInputProcessor.spec.ts diff --git a/test/processors/AvroSchemaInputProcessor.spec.ts b/test/processors/AvroSchemaInputProcessor.spec.ts new file mode 100644 index 0000000000..022304db6a --- /dev/null +++ b/test/processors/AvroSchemaInputProcessor.spec.ts @@ -0,0 +1,49 @@ +import { InputMetaModel } from '../../src/models'; +import { AvroSchemaInputProcessor } from '../../src/processors'; + +describe('AvroSchemaInputProcessor', () => { + describe('shouldProcess()', () => { + test('should fail correctly for empty string input', () => { + const processor = new AvroSchemaInputProcessor(); + expect(processor.shouldProcess('')).toBeFalsy(); + }); + + test('should fail correctly for empty object input', () => { + const processor = new AvroSchemaInputProcessor(); + expect(processor.shouldProcess({})).toBeFalsy(); + }); + + test('should fail correctly for empty array input', () => { + const processor = new AvroSchemaInputProcessor(); + expect(processor.shouldProcess([])).toBeFalsy(); + }); + + test('should fail if input has no type property', () => { + const processor = new AvroSchemaInputProcessor(); + expect(processor.shouldProcess({ someKey: 'someValue' })).toBeFalsy(); + }); + + test('should return true for valid input', () => { + const processor = new AvroSchemaInputProcessor(); + const result = processor.shouldProcess({ type: 'someType' }); + expect(result).toBeTruthy(); + }); + }); + + describe('process()', () => { + test('should throw error when trying to process wrong schema', async () => { + const processor = new AvroSchemaInputProcessor(); + // const invalidInput = { someKey: 'someValue' }; + await expect(processor.process({ someKey: 'someValue' })).rejects.toThrow( + 'Input is not an Avro Schema, so it cannot be processed.' + ); + }); + + test('should process Avro Schema', async () => { + const processor = new AvroSchemaInputProcessor(); + // const validInput = { type: 'someType' }; + const result = await processor.process({ type: 'someType' }); + expect(result).toBeInstanceOf(InputMetaModel); + }); + }); +}); From 34d707cba729a64e7c73c1ae6d90659b1e5da9fd Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Sat, 10 Feb 2024 19:45:44 +0530 Subject: [PATCH 04/13] convert avro schema to meta model --- src/helpers/AvroToMetaModel.ts | 435 +++++++++++++++++++++ src/models/AvroSchema.ts | 33 +- src/processors/AvroSchemaInputProcessor.ts | 24 +- 3 files changed, 477 insertions(+), 15 deletions(-) create mode 100644 src/helpers/AvroToMetaModel.ts diff --git a/src/helpers/AvroToMetaModel.ts b/src/helpers/AvroToMetaModel.ts new file mode 100644 index 0000000000..cec9e06f68 --- /dev/null +++ b/src/helpers/AvroToMetaModel.ts @@ -0,0 +1,435 @@ +import { + AnyModel, + ArrayModel, + AvroSchema, + BooleanModel, + EnumModel, + EnumValueModel, + FloatModel, + IntegerModel, + MetaModel, + MetaModelOptions, + ObjectModel, + ObjectPropertyModel, + StringModel, + UnionModel +} from '../models'; +import { Logger } from '../utils'; + +function getMetaModelOptions(AvroModel: AvroSchema): MetaModelOptions { + const options: MetaModelOptions = {}; + + if (AvroModel.const) { + options.const = { + originalInput: AvroModel.const + }; + } + if (Array.isArray(AvroModel.type) && AvroModel.type.includes('null')) { + options.isNullable = true; + } else { + options.isNullable = false; + } + if (AvroModel.discriminator) { + options.discriminator = { + discriminator: AvroModel.discriminator + }; + } + if (AvroModel.format) { + options.format = AvroModel.format; + } + + return options; +} + +export function AvroToMetaModel( + avroSchemaModel: AvroSchema, + alreadySeenModels: Map = new Map() +): MetaModel { + const hasModel = alreadySeenModels.has(avroSchemaModel); + if (hasModel) { + return alreadySeenModels.get(avroSchemaModel) as MetaModel; + } + + const modelName = avroSchemaModel.name || 'undefined'; + + const objectModel = toObjectModel( + avroSchemaModel, + modelName, + alreadySeenModels + ); + if (objectModel !== undefined) { + return objectModel; + } + const arrayModel = toArrayModel( + avroSchemaModel, + modelName, + alreadySeenModels + ); + if (arrayModel !== undefined) { + return arrayModel; + } + const booleanModel = toBooleanModel(avroSchemaModel, modelName); + if (booleanModel !== undefined) { + return booleanModel; + } + const stringModel = toStringModel(avroSchemaModel, modelName); + if (stringModel !== undefined) { + return stringModel; + } + const integerModel = toIntegerModel(avroSchemaModel, modelName); + if (integerModel !== undefined) { + return integerModel; + } + const floatModel = toFloatModel(avroSchemaModel, modelName); + if (floatModel !== undefined) { + return floatModel; + } + const enumModel = toEnumModel(avroSchemaModel, modelName); + if (enumModel !== undefined) { + return enumModel; + } + const unionModel = toUnionModel( + avroSchemaModel, + modelName, + alreadySeenModels + ); + if (unionModel !== undefined) { + return unionModel; + } + + Logger.warn('Failed to convert to MetaModel, defaulting to AnyModel.'); + return new AnyModel( + modelName, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); +} + +export function toBooleanModel( + avroSchemaModel: AvroSchema, + name: string +): BooleanModel | undefined { + if (!avroSchemaModel.type?.includes('boolean')) { + return undefined; + } + return new BooleanModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); +} +export function toIntegerModel( + avroSchemaModel: AvroSchema, + name: string +): IntegerModel | undefined { + if ( + !avroSchemaModel.type?.includes('int') || + !avroSchemaModel.type?.includes('long') + ) { + return undefined; + } + return new IntegerModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); +} +export function toFloatModel( + avroSchemaModel: AvroSchema, + name: string +): FloatModel | undefined { + if ( + !avroSchemaModel.type?.includes('float') || + !avroSchemaModel.type?.includes('double') + ) { + return undefined; + } + return new FloatModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); +} +export function toStringModel( + avroSchemaModel: AvroSchema, + name: string +): StringModel | undefined { + if ( + !avroSchemaModel.type?.includes('string') || + !avroSchemaModel.type?.includes('bytes') + ) { + return undefined; + } + return new StringModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); +} +export function toEnumModel( + avroSchemaModel: AvroSchema, + name: string +): EnumModel | undefined { + if ( + !avroSchemaModel.type?.includes('enum') || + !Array.isArray(avroSchemaModel.symbols) + ) { + return undefined; + } + const enumValueToEnumValueModel = (enumValue: unknown): EnumValueModel => { + if (typeof enumValue !== 'string') { + return new EnumValueModel(JSON.stringify(enumValue), enumValue); + } + return new EnumValueModel(enumValue, enumValue); + }; + + const metaModel = new EnumModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + [] + ); + + if (avroSchemaModel.symbols) { + for (const enumValue of avroSchemaModel.symbols) { + metaModel.values.push(enumValueToEnumValueModel(enumValue)); + } + } + return metaModel; +} +// checks if the typeof value can have any type +function shouldBeAnyType(avroSchemaModel: AvroSchema): boolean { + // check the type array for the any type + const containsAllTypesButNotNull = + Array.isArray(avroSchemaModel.type) && + avroSchemaModel.type.length >= 6 && + !avroSchemaModel.type.includes('null'); + const containsAllTypes = + (Array.isArray(avroSchemaModel.type) && + avroSchemaModel.type.length === 7) || + containsAllTypesButNotNull; + return containsAllTypesButNotNull || containsAllTypes; +} +/** + * Converts a CommonModel into multiple models wrapped in a union model. + * + * Because a CommonModel might contain multiple models, it's name for each of those models would be the same, instead we slightly change the model name. + * Each model has it's type as a name prepended to the union name. + * + * If the CommonModel has multiple types + */ +// eslint-disable-next-line sonarjs/cognitive-complexity +export function toUnionModel( + avroSchemaModel: AvroSchema, + name: string, + alreadySeenModels: Map +): UnionModel | undefined { + const containsUnions = Array.isArray(avroSchemaModel.type); + + // Should not create union from two types where one is null + const containsTypeWithNull = + Array.isArray(avroSchemaModel.type) && + avroSchemaModel.type.length === 2 && + avroSchemaModel.type.includes('null'); + const containsSimpleTypeUnion = + Array.isArray(avroSchemaModel.type) && + avroSchemaModel.type.length > 1 && + !containsTypeWithNull; + const isAnyType = shouldBeAnyType(avroSchemaModel); + + //Lets see whether we should have a union or not. + if ( + (!containsSimpleTypeUnion && !containsUnions) || + Array.isArray(avroSchemaModel.type) || + isAnyType || + containsTypeWithNull + ) { + return undefined; + } + const unionModel = new UnionModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + [] + ); + + //cache model before continuing + if (!alreadySeenModels.has(avroSchemaModel)) { + alreadySeenModels.set(avroSchemaModel, unionModel); + } + + // Has multiple types, so convert to union + if (containsUnions && Array.isArray(avroSchemaModel.type)) { + for (const unionCommonModel of avroSchemaModel.type) { + const isSingleNullType = + (Array.isArray(unionCommonModel.type) && + unionCommonModel.type.length === 1 && + unionCommonModel.type?.includes('null')) || + unionCommonModel.type === 'null'; + if (isSingleNullType) { + unionModel.options.isNullable = true; + } else { + const unionMetaModel = AvroToMetaModel( + unionCommonModel, + alreadySeenModels + ); + unionModel.union.push(unionMetaModel); + } + } + return unionModel; + } + + // Has simple union types + // Each must have a different name then the root union model, as it otherwise clashes when code is generated + const enumModel = toEnumModel(avroSchemaModel, `${name}_enum`); + if (enumModel !== undefined) { + unionModel.union.push(enumModel); + } + const objectModel = toObjectModel( + avroSchemaModel, + `${name}_object`, + alreadySeenModels + ); + if (objectModel !== undefined) { + unionModel.union.push(objectModel); + } + // const dictionaryModel = toDictionaryModel( + // avroSchemaModel, + // `${name}_dictionary`, + // alreadySeenModels + // ); + // if (dictionaryModel !== undefined) { + // unionModel.union.push(dictionaryModel); + // } + // const tupleModel = toTupleModel( + // avroSchemaModel, + // `${name}_tuple`, + // alreadySeenModels + // ); + // if (tupleModel !== undefined) { + // unionModel.union.push(tupleModel); + // } + const arrayModel = toArrayModel( + avroSchemaModel, + `${name}_array`, + alreadySeenModels + ); + if (arrayModel !== undefined) { + unionModel.union.push(arrayModel); + } + const stringModel = toStringModel(avroSchemaModel, `${name}_string`); + if (stringModel !== undefined) { + unionModel.union.push(stringModel); + } + const floatModel = toFloatModel(avroSchemaModel, `${name}_float`); + if (floatModel !== undefined) { + unionModel.union.push(floatModel); + } + const integerModel = toIntegerModel(avroSchemaModel, `${name}_integer`); + if (integerModel !== undefined) { + unionModel.union.push(integerModel); + } + const booleanModel = toBooleanModel(avroSchemaModel, `${name}_boolean`); + if (booleanModel !== undefined) { + unionModel.union.push(booleanModel); + } + return unionModel; +} +export function toObjectModel( + avroSchemaModel: AvroSchema, + name: string, + alreadySeenModels: Map +): ObjectModel | undefined { + if (!avroSchemaModel.type?.includes('record')) { + return undefined; + } + const metaModel = new ObjectModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + {} + ); + // cache model before continuing + if (!alreadySeenModels.has(avroSchemaModel)) { + alreadySeenModels.set(avroSchemaModel, metaModel); + } + + // fields: a required attribute of record and a JSON Array of JSON Objects + for (const prop of avroSchemaModel?.fields || []) { + const isRequired = avroSchemaModel.isRequired(prop.name); + const propertyModel = new ObjectPropertyModel( + prop.name ?? '', + isRequired, + AvroToMetaModel(prop, alreadySeenModels) + ); + metaModel.properties[String(prop.name)] = propertyModel; + } + + if (avroSchemaModel.extend?.length) { + metaModel.options.extend = []; + + for (const extend of avroSchemaModel.extend) { + metaModel.options.extend.push(AvroToMetaModel(extend, alreadySeenModels)); + } + } + + return metaModel; +} +export function toArrayModel( + avroSchemaModel: AvroSchema, + name: string, + alreadySeenModels: Map +): ArrayModel | undefined { + if (!avroSchemaModel.type?.includes('array')) { + return undefined; + } + const isNormalArray = !Array.isArray(avroSchemaModel.items); + //items single type = normal array + //items not sat = normal array, any type + if (isNormalArray) { + const placeholderModel = new AnyModel( + '', + undefined, + getMetaModelOptions(avroSchemaModel) + ); + const metaModel = new ArrayModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + placeholderModel + ); + alreadySeenModels.set(avroSchemaModel, metaModel); + if (avroSchemaModel.items !== undefined) { + const valueModel = AvroToMetaModel( + avroSchemaModel.items as AvroSchema, + alreadySeenModels + ); + metaModel.valueModel = valueModel; + } + return metaModel; + } + + const valueModel = new UnionModel( + 'union', + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + [] + ); + const metaModel = new ArrayModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + valueModel + ); + alreadySeenModels.set(avroSchemaModel, metaModel); + if (avroSchemaModel.items !== undefined) { + for (const itemModel of Array.isArray(avroSchemaModel.items) + ? avroSchemaModel.items + : [avroSchemaModel.items]) { + const itemsModel = AvroToMetaModel(itemModel, alreadySeenModels); + valueModel.union.push(itemsModel); + } + } + + return metaModel; +} diff --git a/src/models/AvroSchema.ts b/src/models/AvroSchema.ts index b6fc835037..5f0365dfb2 100644 --- a/src/models/AvroSchema.ts +++ b/src/models/AvroSchema.ts @@ -2,26 +2,22 @@ * Avro Schema model */ -// clear unknown to make code more robust - -export interface AvroField { - name?: string; - doc?: string; - type?: unknown; - default?: unknown; -} - export class AvroSchema { - type?: string | string[] | object; + type?: string | string[]; name?: string; namespace?: string; + originalInput?: any; + const?: string; + discriminator?: string; + format?: string; + required?: string[]; + extend?: AvroSchema[]; doc?: string; aliases?: string[]; symbols?: string[]; items?: unknown; values?: unknown; - fields?: AvroField[]; - order?: 'ascending' | 'descending' | 'ignore'; + fields?: AvroSchema[]; size?: number; example?: string | number; minimum?: number; @@ -84,4 +80,17 @@ export class AvroSchema { } return schema; } + + /** + * Checks if given property name is required in object + * + * @param propertyName given property name + * @returns {boolean} + */ + isRequired(propertyName: any): boolean { + if (this.required === undefined) { + return false; + } + return this.required.includes(propertyName); + } } diff --git a/src/processors/AvroSchemaInputProcessor.ts b/src/processors/AvroSchemaInputProcessor.ts index 937858359a..cfe7b9a2f6 100644 --- a/src/processors/AvroSchemaInputProcessor.ts +++ b/src/processors/AvroSchemaInputProcessor.ts @@ -1,11 +1,29 @@ import { AbstractInputProcessor } from './AbstractInputProcessor'; import { InputMetaModel } from '../models'; import { Logger } from '../utils'; -import { convertToMetaModel } from '../helpers'; +import { AvroToMetaModel } from '../helpers/AvroToMetaModel'; /** * Class for processing Avro Schema input */ + +const avroType = [ + 'null', + 'boolean', + 'int', + 'long', + 'double', + 'float', + 'string', + 'bytes', + 'records', + 'enums', + 'arrays', + 'maps', + 'unions', + 'fixed' +]; + export class AvroSchemaInputProcessor extends AbstractInputProcessor { /** * Function processing an Avro Schema input @@ -21,7 +39,7 @@ export class AvroSchemaInputProcessor extends AbstractInputProcessor { ) { return false; } - if (!input.type) { + if (!avroType.includes(input.type) || !input.name) { return false; } return true; @@ -36,7 +54,7 @@ export class AvroSchemaInputProcessor extends AbstractInputProcessor { Logger.debug('Processing input as Avro Schema document'); const inputModel = new InputMetaModel(); inputModel.originalInput = input; - const metaModel = convertToMetaModel(input); + const metaModel = AvroToMetaModel(input); inputModel.models[metaModel.name] = metaModel; Logger.debug('Completed processing input as Avro Schema document'); From c508005b18e3025fd7b79ef913b023afdc5fc829 Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:37:14 +0530 Subject: [PATCH 05/13] test: add tests and snapshot for avro processor --- src/helpers/AvroToMetaModel.ts | 99 +++----- src/helpers/index.ts | 1 + src/models/AvroSchema.ts | 7 +- src/processors/AvroSchemaInputProcessor.ts | 11 +- test/helpers/AvroToMetaModel.spec.ts | 169 +++++++++++++ .../AvroSchemaInputProcessor.spec.ts | 40 ++- .../AvroSchemaInputProcessor/basic.json | 68 ++++++ test/processors/InputProcessor.spec.ts | 31 ++- .../AvroSchemaInputProcessor.spec.ts.snap | 231 ++++++++++++++++++ 9 files changed, 562 insertions(+), 95 deletions(-) create mode 100644 test/helpers/AvroToMetaModel.spec.ts create mode 100644 test/processors/AvroSchemaInputProcessor/basic.json create mode 100644 test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap diff --git a/src/helpers/AvroToMetaModel.ts b/src/helpers/AvroToMetaModel.ts index cec9e06f68..006c08567a 100644 --- a/src/helpers/AvroToMetaModel.ts +++ b/src/helpers/AvroToMetaModel.ts @@ -19,28 +19,26 @@ import { Logger } from '../utils'; function getMetaModelOptions(AvroModel: AvroSchema): MetaModelOptions { const options: MetaModelOptions = {}; - if (AvroModel.const) { - options.const = { - originalInput: AvroModel.const - }; - } - if (Array.isArray(AvroModel.type) && AvroModel.type.includes('null')) { + if (Array.isArray(AvroModel.type) && AvroModel.type !== null) { options.isNullable = true; } else { options.isNullable = false; } - if (AvroModel.discriminator) { - options.discriminator = { - discriminator: AvroModel.discriminator - }; - } - if (AvroModel.format) { - options.format = AvroModel.format; - } return options; } +function shouldBeAnyType(avroSchemaModel: AvroSchema): boolean { + // check the type array for the any type + const containsAllTypesButNotNull = + Array.isArray(avroSchemaModel.type) && + avroSchemaModel.type.length >= 8 && + avroSchemaModel.type !== null; + const containsAllTypes = + Array.isArray(avroSchemaModel.type) && avroSchemaModel.type.length === 10; + return containsAllTypesButNotNull || containsAllTypes; +} + export function AvroToMetaModel( avroSchemaModel: AvroSchema, alreadySeenModels: Map = new Map() @@ -52,6 +50,13 @@ export function AvroToMetaModel( const modelName = avroSchemaModel.name || 'undefined'; + if (shouldBeAnyType(avroSchemaModel)) { + return new AnyModel( + modelName, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); + } const objectModel = toObjectModel( avroSchemaModel, modelName, @@ -109,7 +114,7 @@ export function toBooleanModel( avroSchemaModel: AvroSchema, name: string ): BooleanModel | undefined { - if (!avroSchemaModel.type?.includes('boolean')) { + if (avroSchemaModel.type !== 'boolean') { return undefined; } return new BooleanModel( @@ -122,10 +127,7 @@ export function toIntegerModel( avroSchemaModel: AvroSchema, name: string ): IntegerModel | undefined { - if ( - !avroSchemaModel.type?.includes('int') || - !avroSchemaModel.type?.includes('long') - ) { + if (avroSchemaModel.type !== 'int' && avroSchemaModel.type !== 'long') { return undefined; } return new IntegerModel( @@ -138,10 +140,7 @@ export function toFloatModel( avroSchemaModel: AvroSchema, name: string ): FloatModel | undefined { - if ( - !avroSchemaModel.type?.includes('float') || - !avroSchemaModel.type?.includes('double') - ) { + if (avroSchemaModel.type !== 'float' && avroSchemaModel.type !== 'double') { return undefined; } return new FloatModel( @@ -154,10 +153,7 @@ export function toStringModel( avroSchemaModel: AvroSchema, name: string ): StringModel | undefined { - if ( - !avroSchemaModel.type?.includes('string') || - !avroSchemaModel.type?.includes('bytes') - ) { + if (avroSchemaModel.type !== 'string') { return undefined; } return new StringModel( @@ -171,7 +167,7 @@ export function toEnumModel( name: string ): EnumModel | undefined { if ( - !avroSchemaModel.type?.includes('enum') || + avroSchemaModel.type !== 'enum' || !Array.isArray(avroSchemaModel.symbols) ) { return undefined; @@ -197,27 +193,6 @@ export function toEnumModel( } return metaModel; } -// checks if the typeof value can have any type -function shouldBeAnyType(avroSchemaModel: AvroSchema): boolean { - // check the type array for the any type - const containsAllTypesButNotNull = - Array.isArray(avroSchemaModel.type) && - avroSchemaModel.type.length >= 6 && - !avroSchemaModel.type.includes('null'); - const containsAllTypes = - (Array.isArray(avroSchemaModel.type) && - avroSchemaModel.type.length === 7) || - containsAllTypesButNotNull; - return containsAllTypesButNotNull || containsAllTypes; -} -/** - * Converts a CommonModel into multiple models wrapped in a union model. - * - * Because a CommonModel might contain multiple models, it's name for each of those models would be the same, instead we slightly change the model name. - * Each model has it's type as a name prepended to the union name. - * - * If the CommonModel has multiple types - */ // eslint-disable-next-line sonarjs/cognitive-complexity export function toUnionModel( avroSchemaModel: AvroSchema, @@ -293,30 +268,14 @@ export function toUnionModel( if (objectModel !== undefined) { unionModel.union.push(objectModel); } - // const dictionaryModel = toDictionaryModel( + // const arrayModel = toArrayModel( // avroSchemaModel, - // `${name}_dictionary`, + // `${name}_array`, // alreadySeenModels // ); - // if (dictionaryModel !== undefined) { - // unionModel.union.push(dictionaryModel); + // if (arrayModel !== undefined) { + // unionModel.union.push(arrayModel); // } - // const tupleModel = toTupleModel( - // avroSchemaModel, - // `${name}_tuple`, - // alreadySeenModels - // ); - // if (tupleModel !== undefined) { - // unionModel.union.push(tupleModel); - // } - const arrayModel = toArrayModel( - avroSchemaModel, - `${name}_array`, - alreadySeenModels - ); - if (arrayModel !== undefined) { - unionModel.union.push(arrayModel); - } const stringModel = toStringModel(avroSchemaModel, `${name}_string`); if (stringModel !== undefined) { unionModel.union.push(stringModel); @@ -340,7 +299,7 @@ export function toObjectModel( name: string, alreadySeenModels: Map ): ObjectModel | undefined { - if (!avroSchemaModel.type?.includes('record')) { + if (avroSchemaModel.type !== 'record') { return undefined; } const metaModel = new ObjectModel( diff --git a/src/helpers/index.ts b/src/helpers/index.ts index c1c75769dc..b587bd6483 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,3 +8,4 @@ export * from './ConstrainHelpers'; export * from './PresetHelpers'; export * from './DependencyHelpers'; export * from './FilterHelpers'; +export * from './AvroToMetaModel'; diff --git a/src/models/AvroSchema.ts b/src/models/AvroSchema.ts index 5f0365dfb2..008ab42939 100644 --- a/src/models/AvroSchema.ts +++ b/src/models/AvroSchema.ts @@ -8,17 +8,12 @@ export class AvroSchema { namespace?: string; originalInput?: any; const?: string; - discriminator?: string; - format?: string; required?: string[]; - extend?: AvroSchema[]; doc?: string; aliases?: string[]; symbols?: string[]; - items?: unknown; - values?: unknown; + items?: string; fields?: AvroSchema[]; - size?: number; example?: string | number; minimum?: number; maximum?: number; diff --git a/src/processors/AvroSchemaInputProcessor.ts b/src/processors/AvroSchemaInputProcessor.ts index cfe7b9a2f6..8c41257cb5 100644 --- a/src/processors/AvroSchemaInputProcessor.ts +++ b/src/processors/AvroSchemaInputProcessor.ts @@ -15,13 +15,10 @@ const avroType = [ 'double', 'float', 'string', - 'bytes', - 'records', - 'enums', - 'arrays', - 'maps', - 'unions', - 'fixed' + 'record', + 'enum', + 'array', + 'map' ]; export class AvroSchemaInputProcessor extends AbstractInputProcessor { diff --git a/test/helpers/AvroToMetaModel.spec.ts b/test/helpers/AvroToMetaModel.spec.ts new file mode 100644 index 0000000000..8f96b47f8f --- /dev/null +++ b/test/helpers/AvroToMetaModel.spec.ts @@ -0,0 +1,169 @@ +import { + AnyModel, + AvroSchema, + BooleanModel, + EnumModel, + FloatModel, + IntegerModel, + ObjectModel, + StringModel +} from '../../src'; +import { AvroToMetaModel } from '../../src/helpers'; + +describe('AvroToMetaModel', () => { + describe('nullable', () => { + test('should apply null type', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = ['string', 'null']; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof StringModel).toBeTruthy(); + expect(model.options.isNullable).toBeTruthy(); + }); + test('should not apply null type', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = ['string']; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof StringModel).toBeTruthy(); + expect(model.options.isNullable).toBeFalsy(); + }); + }); + test('should default to any model', () => { + const av = new AvroSchema(); + av.name = 'test'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof AnyModel).toBeTruthy(); + }); + test('should convert to any model', () => { + const av = new AvroSchema(); + av.type = [ + 'string', + 'float', + 'int', + 'double', + 'enum', + 'long', + 'boolean', + 'record', + 'array', + 'null' + ]; + av.name = 'test'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof AnyModel).toBeTruthy(); + }); + test('should convert to any model with missing null', () => { + const av = new AvroSchema(); + av.type = [ + 'string', + 'float', + 'int', + 'double', + 'enum', + 'long', + 'boolean', + 'record', + 'array' + ]; + av.name = 'test'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof AnyModel).toBeTruthy(); + }); + test('should convert to boolean model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'boolean'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof BooleanModel).toEqual(true); + }); + test('should convert to string model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'string'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof StringModel).toBeTruthy(); + }); + test('should convert type float to float model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'float'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof FloatModel).toBeTruthy(); + }); + test('should convert type double to float model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'double'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof FloatModel).toBeTruthy(); + }); + test('should convert type long to integer model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'long'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof IntegerModel).toBeTruthy(); + }); + test('should convert type int to integer model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'int'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof IntegerModel).toBeTruthy(); + }); + test('should convert to object model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'record'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof ObjectModel).toBeTruthy(); + }); + test('should convert to enum model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'enum'; + av.symbols = ['hello', '2']; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof EnumModel).toBeTruthy(); + }); +}); diff --git a/test/processors/AvroSchemaInputProcessor.spec.ts b/test/processors/AvroSchemaInputProcessor.spec.ts index 022304db6a..e166dd5bd6 100644 --- a/test/processors/AvroSchemaInputProcessor.spec.ts +++ b/test/processors/AvroSchemaInputProcessor.spec.ts @@ -1,39 +1,58 @@ -import { InputMetaModel } from '../../src/models'; +import * as fs from 'fs'; +import * as path from 'path'; import { AvroSchemaInputProcessor } from '../../src/processors'; +const basicDoc = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, './AvroSchemaInputProcessor/basic.json'), + 'utf8' + ) +); describe('AvroSchemaInputProcessor', () => { describe('shouldProcess()', () => { + const processor = new AvroSchemaInputProcessor(); test('should fail correctly for empty string input', () => { - const processor = new AvroSchemaInputProcessor(); expect(processor.shouldProcess('')).toBeFalsy(); }); test('should fail correctly for empty object input', () => { - const processor = new AvroSchemaInputProcessor(); expect(processor.shouldProcess({})).toBeFalsy(); }); test('should fail correctly for empty array input', () => { - const processor = new AvroSchemaInputProcessor(); expect(processor.shouldProcess([])).toBeFalsy(); }); + test('should fail if input has no name property', () => { + expect( + processor.shouldProcess({ prop: 'hello', type: 'string' }) + ).toBeFalsy(); + }); + test('should fail if input has no type property', () => { - const processor = new AvroSchemaInputProcessor(); - expect(processor.shouldProcess({ someKey: 'someValue' })).toBeFalsy(); + expect( + processor.shouldProcess({ name: 'hello', someKey: 'someValue' }) + ).toBeFalsy(); }); test('should return true for valid input', () => { const processor = new AvroSchemaInputProcessor(); - const result = processor.shouldProcess({ type: 'someType' }); + const result = processor.shouldProcess({ name: 'hello', type: 'string' }); expect(result).toBeTruthy(); }); + test('should fail for invalid type', () => { + const processor = new AvroSchemaInputProcessor(); + const result = processor.shouldProcess({ + name: 'hello', + type: 'someValue' + }); + expect(result).toBeFalsy(); + }); }); describe('process()', () => { test('should throw error when trying to process wrong schema', async () => { const processor = new AvroSchemaInputProcessor(); - // const invalidInput = { someKey: 'someValue' }; await expect(processor.process({ someKey: 'someValue' })).rejects.toThrow( 'Input is not an Avro Schema, so it cannot be processed.' ); @@ -41,9 +60,8 @@ describe('AvroSchemaInputProcessor', () => { test('should process Avro Schema', async () => { const processor = new AvroSchemaInputProcessor(); - // const validInput = { type: 'someType' }; - const result = await processor.process({ type: 'someType' }); - expect(result).toBeInstanceOf(InputMetaModel); + const result = await processor.process(basicDoc); + expect(result).toMatchSnapshot(); }); }); }); diff --git a/test/processors/AvroSchemaInputProcessor/basic.json b/test/processors/AvroSchemaInputProcessor/basic.json new file mode 100644 index 0000000000..f96290e884 --- /dev/null +++ b/test/processors/AvroSchemaInputProcessor/basic.json @@ -0,0 +1,68 @@ +{ + "name": "Person", + "namespace": "com.company", + "type": "record", + "fields": [ + { "name": "name", "type": "string", "example": "Donkey", "minLength": 0 }, + { "name": "serialNo", "type": "string", "minLength": 0, "maxLength": 50 }, + { + "name": "email", + "type": ["null", "string"], + "example": "donkey@asyncapi.com", + "pattern": "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$" + }, + { + "name": "age", + "type": ["null", "int"], + "default": null, + "example": 123, + "exclusiveMinimum": 0, + "exclusiveMaximum": 200 + }, + { + "name": "favoriteProgrammingLanguage", + "type": { + "name": "ProgrammingLanguage", + "type": "enum", + "symbols": ["JS", "Java", "Go", "Rust", "C"], + "default": "JS" + } + }, + { + "name": "certifications", + "type": { + "type": "array", + "items": "string", + "minItems": 1, + "maxItems": 500, + "uniqueItems": true + } + }, + { + "name": "address", + "type": { + "name": "Address", + "type": "record", + "fields": [ + { "name": "zipcode", "type": "int", "example": 53003 }, + { "name": "country", "type": ["null", "string"] } + ] + } + }, + { + "name": "weight", + "type": "float", + "example": 65.1, + "minimum": 0, + "maximum": 500 + }, + { + "name": "height", + "type": "double", + "example": 1.85, + "minimum": 0, + "maximum": 3.0 + }, + { "name": "someid", "type": "string", "logicalType": "uuid" } + ] +} diff --git a/test/processors/InputProcessor.spec.ts b/test/processors/InputProcessor.spec.ts index f09b22eeb2..cc974a3b7f 100644 --- a/test/processors/InputProcessor.spec.ts +++ b/test/processors/InputProcessor.spec.ts @@ -4,6 +4,7 @@ import { InputMetaModel, ProcessorOptions } from '../../src/models'; import { AbstractInputProcessor, AsyncAPIInputProcessor, + AvroSchemaInputProcessor, JsonSchemaInputProcessor, InputProcessor, SwaggerInputProcessor @@ -58,6 +59,9 @@ describe('InputProcessor', () => { const asyncInputProcessor = new AsyncAPIInputProcessor(); jest.spyOn(asyncInputProcessor, 'shouldProcess'); jest.spyOn(asyncInputProcessor, 'process'); + const avroSchemaInputProcessor = new AvroSchemaInputProcessor(); + jest.spyOn(avroSchemaInputProcessor, 'shouldProcess'); + jest.spyOn(avroSchemaInputProcessor, 'process'); const defaultInputProcessor = new JsonSchemaInputProcessor(); jest.spyOn(defaultInputProcessor, 'shouldProcess'); jest.spyOn(defaultInputProcessor, 'process'); @@ -70,12 +74,14 @@ describe('InputProcessor', () => { const processor = new InputProcessor(); processor.setProcessor('asyncapi', asyncInputProcessor); processor.setProcessor('swagger', swaggerInputProcessor); + processor.setProcessor('avroSchema', avroSchemaInputProcessor); processor.setProcessor('openapi', openAPIInputProcessor); processor.setProcessor('default', defaultInputProcessor); return { processor, asyncInputProcessor, swaggerInputProcessor, + avroSchemaInputProcessor, openAPIInputProcessor, defaultInputProcessor }; @@ -177,7 +183,30 @@ describe('InputProcessor', () => { expect(defaultInputProcessor.process).not.toHaveBeenCalled(); expect(defaultInputProcessor.shouldProcess).not.toHaveBeenCalled(); }); - + test('should be able to process Avro Schema input', async () => { + const { processor, avroSchemaInputProcessor, defaultInputProcessor } = + getProcessors(); + const inputSchemaString = fs.readFileSync( + path.resolve(__dirname, './AvroSchemaInputProcessor/sample.json'), + 'utf8' + ); + const inputSchema = JSON.parse(inputSchemaString); + await processor.process(inputSchema); + expect(avroSchemaInputProcessor.process).not.toHaveBeenCalled(); + expect(avroSchemaInputProcessor.shouldProcess).toHaveBeenNthCalledWith( + 1, + inputSchema + ); + expect(defaultInputProcessor.process).toHaveBeenNthCalledWith( + 1, + inputSchema, + undefined + ); + expect(defaultInputProcessor.shouldProcess).toHaveBeenNthCalledWith( + 1, + inputSchema + ); + }); test('should be able to process AsyncAPI schema input with options', async () => { const { processor, asyncInputProcessor, defaultInputProcessor } = getProcessors(); diff --git a/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap b/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap new file mode 100644 index 0000000000..db0fd50506 --- /dev/null +++ b/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap @@ -0,0 +1,231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AvroSchemaInputProcessor process() should process Avro Schema 1`] = ` +InputMetaModel { + "models": Object { + "Person": ObjectModel { + "name": "Person", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + "properties": Object { + "address": ObjectPropertyModel { + "property": AnyModel { + "name": "address", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "address", + "required": true, + }, + "age": ObjectPropertyModel { + "property": AnyModel { + "name": "age", + "options": Object { + "isNullable": true, + }, + "originalInput": undefined, + }, + "propertyName": "age", + "required": true, + }, + "certifications": ObjectPropertyModel { + "property": AnyModel { + "name": "certifications", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "certifications", + "required": true, + }, + "email": ObjectPropertyModel { + "property": AnyModel { + "name": "email", + "options": Object { + "isNullable": true, + }, + "originalInput": undefined, + }, + "propertyName": "email", + "required": true, + }, + "favoriteProgrammingLanguage": ObjectPropertyModel { + "property": AnyModel { + "name": "favoriteProgrammingLanguage", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "favoriteProgrammingLanguage", + "required": true, + }, + "height": ObjectPropertyModel { + "property": FloatModel { + "name": "height", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "height", + "required": true, + }, + "name": ObjectPropertyModel { + "property": StringModel { + "name": "name", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "name", + "required": true, + }, + "serialNo": ObjectPropertyModel { + "property": StringModel { + "name": "serialNo", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "serialNo", + "required": true, + }, + "someid": ObjectPropertyModel { + "property": StringModel { + "name": "someid", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "someid", + "required": true, + }, + "weight": ObjectPropertyModel { + "property": FloatModel { + "name": "weight", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "weight", + "required": true, + }, + }, + }, + }, + "originalInput": Object { + "fields": Array [ + Object { + "example": "Donkey", + "minLength": 0, + "name": "name", + "type": "string", + }, + Object { + "maxLength": 50, + "minLength": 0, + "name": "serialNo", + "type": "string", + }, + Object { + "example": "donkey@asyncapi.com", + "name": "email", + "pattern": "^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$", + "type": Array [ + "null", + "string", + ], + }, + Object { + "default": null, + "example": 123, + "exclusiveMaximum": 200, + "exclusiveMinimum": 0, + "name": "age", + "type": Array [ + "null", + "int", + ], + }, + Object { + "name": "favoriteProgrammingLanguage", + "type": Object { + "default": "JS", + "name": "ProgrammingLanguage", + "symbols": Array [ + "JS", + "Java", + "Go", + "Rust", + "C", + ], + "type": "enum", + }, + }, + Object { + "name": "certifications", + "type": Object { + "items": "string", + "maxItems": 500, + "minItems": 1, + "type": "array", + "uniqueItems": true, + }, + }, + Object { + "name": "address", + "type": Object { + "fields": Array [ + Object { + "example": 53003, + "name": "zipcode", + "type": "int", + }, + Object { + "name": "country", + "type": Array [ + "null", + "string", + ], + }, + ], + "name": "Address", + "type": "record", + }, + }, + Object { + "example": 65.1, + "maximum": 500, + "minimum": 0, + "name": "weight", + "type": "float", + }, + Object { + "example": 1.85, + "maximum": 3, + "minimum": 0, + "name": "height", + "type": "double", + }, + Object { + "logicalType": "uuid", + "name": "someid", + "type": "string", + }, + ], + "name": "Person", + "namespace": "com.company", + "type": "record", + }, +} +`; From 277ff5332648acadcf70dc83c7835633c3b4ba7e Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:56:02 +0530 Subject: [PATCH 06/13] remove const & required property --- src/models/AvroSchema.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/models/AvroSchema.ts b/src/models/AvroSchema.ts index 008ab42939..9b3fb0f83b 100644 --- a/src/models/AvroSchema.ts +++ b/src/models/AvroSchema.ts @@ -3,12 +3,10 @@ */ export class AvroSchema { - type?: string | string[]; + type?: string | string[] | AvroSchema; name?: string; namespace?: string; originalInput?: any; - const?: string; - required?: string[]; doc?: string; aliases?: string[]; symbols?: string[]; @@ -75,17 +73,4 @@ export class AvroSchema { } return schema; } - - /** - * Checks if given property name is required in object - * - * @param propertyName given property name - * @returns {boolean} - */ - isRequired(propertyName: any): boolean { - if (this.required === undefined) { - return false; - } - return this.required.includes(propertyName); - } } From fc2ab8e025f2d9f858ac51cbd786bb9bfad77564 Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:51:44 +0530 Subject: [PATCH 07/13] feat: add array and union model --- src/helpers/AvroToMetaModel.ts | 292 ++++++++---------- test/helpers/AvroToMetaModel.spec.ts | 24 +- .../AvroSchemaInputProcessor.spec.ts.snap | 69 ++++- 3 files changed, 214 insertions(+), 171 deletions(-) diff --git a/src/helpers/AvroToMetaModel.ts b/src/helpers/AvroToMetaModel.ts index 006c08567a..d92598f1da 100644 --- a/src/helpers/AvroToMetaModel.ts +++ b/src/helpers/AvroToMetaModel.ts @@ -18,13 +18,11 @@ import { Logger } from '../utils'; function getMetaModelOptions(AvroModel: AvroSchema): MetaModelOptions { const options: MetaModelOptions = {}; - - if (Array.isArray(AvroModel.type) && AvroModel.type !== null) { + if (Array.isArray(AvroModel.type) && AvroModel.type?.includes('null')) { options.isNullable = true; } else { options.isNullable = false; } - return options; } @@ -33,7 +31,7 @@ function shouldBeAnyType(avroSchemaModel: AvroSchema): boolean { const containsAllTypesButNotNull = Array.isArray(avroSchemaModel.type) && avroSchemaModel.type.length >= 8 && - avroSchemaModel.type !== null; + !avroSchemaModel.type.includes('null'); const containsAllTypes = Array.isArray(avroSchemaModel.type) && avroSchemaModel.type.length === 10; return containsAllTypesButNotNull || containsAllTypes; @@ -48,7 +46,7 @@ export function AvroToMetaModel( return alreadySeenModels.get(avroSchemaModel) as MetaModel; } - const modelName = avroSchemaModel.name || 'undefined'; + const modelName = avroSchemaModel?.name || 'undefined'; if (shouldBeAnyType(avroSchemaModel)) { return new AnyModel( @@ -57,6 +55,16 @@ export function AvroToMetaModel( getMetaModelOptions(avroSchemaModel) ); } + if ( + avroSchemaModel.type && + !Array.isArray(avroSchemaModel.type) && + typeof avroSchemaModel.type !== 'string' + ) { + return AvroToMetaModel( + avroSchemaModel.type as AvroSchema, + alreadySeenModels + ); + } const objectModel = toObjectModel( avroSchemaModel, modelName, @@ -114,113 +122,125 @@ export function toBooleanModel( avroSchemaModel: AvroSchema, name: string ): BooleanModel | undefined { - if (avroSchemaModel.type !== 'boolean') { - return undefined; + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + avroSchemaModel.type.includes('boolean') + ) { + return new BooleanModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); } - return new BooleanModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel) - ); + return undefined; } export function toIntegerModel( avroSchemaModel: AvroSchema, name: string ): IntegerModel | undefined { - if (avroSchemaModel.type !== 'int' && avroSchemaModel.type !== 'long') { - return undefined; + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + (avroSchemaModel.type.includes('int') || + avroSchemaModel.type.includes('long')) + ) { + return new IntegerModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); } - return new IntegerModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel) - ); + return undefined; } export function toFloatModel( avroSchemaModel: AvroSchema, name: string ): FloatModel | undefined { - if (avroSchemaModel.type !== 'float' && avroSchemaModel.type !== 'double') { - return undefined; + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + (avroSchemaModel.type.includes('float') || + avroSchemaModel.type.includes('double')) + ) { + return new FloatModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); } - return new FloatModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel) - ); + return undefined; } export function toStringModel( avroSchemaModel: AvroSchema, name: string ): StringModel | undefined { - if (avroSchemaModel.type !== 'string') { - return undefined; + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + avroSchemaModel.type.includes('string') + ) { + return new StringModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel) + ); } - return new StringModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel) - ); + return undefined; } export function toEnumModel( avroSchemaModel: AvroSchema, name: string ): EnumModel | undefined { if ( - avroSchemaModel.type !== 'enum' || - !Array.isArray(avroSchemaModel.symbols) + ((typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + avroSchemaModel.type.includes('enum')) || + Array.isArray(avroSchemaModel.symbols) ) { - return undefined; - } - const enumValueToEnumValueModel = (enumValue: unknown): EnumValueModel => { - if (typeof enumValue !== 'string') { - return new EnumValueModel(JSON.stringify(enumValue), enumValue); - } - return new EnumValueModel(enumValue, enumValue); - }; + const enumValueToEnumValueModel = (enumValue: unknown): EnumValueModel => { + if (typeof enumValue !== 'string') { + return new EnumValueModel(JSON.stringify(enumValue), enumValue); + } + return new EnumValueModel(enumValue, enumValue); + }; - const metaModel = new EnumModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel), - [] - ); + const metaModel = new EnumModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + [] + ); - if (avroSchemaModel.symbols) { - for (const enumValue of avroSchemaModel.symbols) { - metaModel.values.push(enumValueToEnumValueModel(enumValue)); + if (avroSchemaModel.symbols) { + for (const enumValue of avroSchemaModel.symbols) { + metaModel.values.push(enumValueToEnumValueModel(enumValue)); + } } + return metaModel; } - return metaModel; + return undefined; } -// eslint-disable-next-line sonarjs/cognitive-complexity export function toUnionModel( avroSchemaModel: AvroSchema, name: string, alreadySeenModels: Map ): UnionModel | undefined { - const containsUnions = Array.isArray(avroSchemaModel.type); + if (!Array.isArray(avroSchemaModel.type)) { + return undefined; + } - // Should not create union from two types where one is null + // Should not create union from two types where one is null, i.e, true for ['string', 'null'] const containsTypeWithNull = Array.isArray(avroSchemaModel.type) && avroSchemaModel.type.length === 2 && avroSchemaModel.type.includes('null'); - const containsSimpleTypeUnion = - Array.isArray(avroSchemaModel.type) && - avroSchemaModel.type.length > 1 && - !containsTypeWithNull; - const isAnyType = shouldBeAnyType(avroSchemaModel); - //Lets see whether we should have a union or not. - if ( - (!containsSimpleTypeUnion && !containsUnions) || - Array.isArray(avroSchemaModel.type) || - isAnyType || - containsTypeWithNull - ) { + if (containsTypeWithNull) { return undefined; } + + // type: ['string', 'int'] const unionModel = new UnionModel( name, avroSchemaModel.originalInput, @@ -233,27 +253,6 @@ export function toUnionModel( alreadySeenModels.set(avroSchemaModel, unionModel); } - // Has multiple types, so convert to union - if (containsUnions && Array.isArray(avroSchemaModel.type)) { - for (const unionCommonModel of avroSchemaModel.type) { - const isSingleNullType = - (Array.isArray(unionCommonModel.type) && - unionCommonModel.type.length === 1 && - unionCommonModel.type?.includes('null')) || - unionCommonModel.type === 'null'; - if (isSingleNullType) { - unionModel.options.isNullable = true; - } else { - const unionMetaModel = AvroToMetaModel( - unionCommonModel, - alreadySeenModels - ); - unionModel.union.push(unionMetaModel); - } - } - return unionModel; - } - // Has simple union types // Each must have a different name then the root union model, as it otherwise clashes when code is generated const enumModel = toEnumModel(avroSchemaModel, `${name}_enum`); @@ -268,14 +267,14 @@ export function toUnionModel( if (objectModel !== undefined) { unionModel.union.push(objectModel); } - // const arrayModel = toArrayModel( - // avroSchemaModel, - // `${name}_array`, - // alreadySeenModels - // ); - // if (arrayModel !== undefined) { - // unionModel.union.push(arrayModel); - // } + const arrayModel = toArrayModel( + avroSchemaModel, + `${name}_array`, + alreadySeenModels + ); + if (arrayModel !== undefined) { + unionModel.union.push(arrayModel); + } const stringModel = toStringModel(avroSchemaModel, `${name}_string`); if (stringModel !== undefined) { unionModel.union.push(stringModel); @@ -299,53 +298,45 @@ export function toObjectModel( name: string, alreadySeenModels: Map ): ObjectModel | undefined { - if (avroSchemaModel.type !== 'record') { - return undefined; - } - const metaModel = new ObjectModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel), - {} - ); - // cache model before continuing - if (!alreadySeenModels.has(avroSchemaModel)) { - alreadySeenModels.set(avroSchemaModel, metaModel); - } - - // fields: a required attribute of record and a JSON Array of JSON Objects - for (const prop of avroSchemaModel?.fields || []) { - const isRequired = avroSchemaModel.isRequired(prop.name); - const propertyModel = new ObjectPropertyModel( - prop.name ?? '', - isRequired, - AvroToMetaModel(prop, alreadySeenModels) + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + avroSchemaModel.type.includes('record') + ) { + const metaModel = new ObjectModel( + name, + avroSchemaModel.originalInput, + getMetaModelOptions(avroSchemaModel), + {} ); - metaModel.properties[String(prop.name)] = propertyModel; - } - - if (avroSchemaModel.extend?.length) { - metaModel.options.extend = []; + // cache model before continuing + if (!alreadySeenModels.has(avroSchemaModel)) { + alreadySeenModels.set(avroSchemaModel, metaModel); + } - for (const extend of avroSchemaModel.extend) { - metaModel.options.extend.push(AvroToMetaModel(extend, alreadySeenModels)); + // fields: a required attribute of record and a JSON Array of JSON Objects + for (const prop of avroSchemaModel?.fields || []) { + const propertyModel = new ObjectPropertyModel( + prop.name ?? '', + true, + AvroToMetaModel(prop, alreadySeenModels) + ); + metaModel.properties[String(prop.name)] = propertyModel; } + return metaModel; } - - return metaModel; + return undefined; } export function toArrayModel( avroSchemaModel: AvroSchema, name: string, alreadySeenModels: Map ): ArrayModel | undefined { - if (!avroSchemaModel.type?.includes('array')) { - return undefined; - } - const isNormalArray = !Array.isArray(avroSchemaModel.items); - //items single type = normal array - //items not sat = normal array, any type - if (isNormalArray) { + if ( + (typeof avroSchemaModel.type === 'string' || + Array.isArray(avroSchemaModel.type)) && + avroSchemaModel.type?.includes('array') + ) { const placeholderModel = new AnyModel( '', undefined, @@ -359,36 +350,13 @@ export function toArrayModel( ); alreadySeenModels.set(avroSchemaModel, metaModel); if (avroSchemaModel.items !== undefined) { - const valueModel = AvroToMetaModel( - avroSchemaModel.items as AvroSchema, - alreadySeenModels - ); + const AvroModel = new AvroSchema(); + AvroModel.name = `${name}_${avroSchemaModel.items}`; + AvroModel.type = avroSchemaModel.items; + const valueModel = AvroToMetaModel(AvroModel, alreadySeenModels); metaModel.valueModel = valueModel; } return metaModel; } - - const valueModel = new UnionModel( - 'union', - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel), - [] - ); - const metaModel = new ArrayModel( - name, - avroSchemaModel.originalInput, - getMetaModelOptions(avroSchemaModel), - valueModel - ); - alreadySeenModels.set(avroSchemaModel, metaModel); - if (avroSchemaModel.items !== undefined) { - for (const itemModel of Array.isArray(avroSchemaModel.items) - ? avroSchemaModel.items - : [avroSchemaModel.items]) { - const itemsModel = AvroToMetaModel(itemModel, alreadySeenModels); - valueModel.union.push(itemsModel); - } - } - - return metaModel; + return undefined; } diff --git a/test/helpers/AvroToMetaModel.spec.ts b/test/helpers/AvroToMetaModel.spec.ts index 8f96b47f8f..299e7901ac 100644 --- a/test/helpers/AvroToMetaModel.spec.ts +++ b/test/helpers/AvroToMetaModel.spec.ts @@ -1,5 +1,6 @@ import { AnyModel, + ArrayModel, AvroSchema, BooleanModel, EnumModel, @@ -12,7 +13,7 @@ import { AvroToMetaModel } from '../../src/helpers'; describe('AvroToMetaModel', () => { describe('nullable', () => { - test('should apply null type', () => { + test('should apply null and string type', () => { const av = new AvroSchema(); av.name = 'test'; av.type = ['string', 'null']; @@ -166,4 +167,25 @@ describe('AvroToMetaModel', () => { expect(model).not.toBeUndefined(); expect(model instanceof EnumModel).toBeTruthy(); }); + test('should convert to array model', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = 'array'; + av.items = 'string'; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof ArrayModel).toBeTruthy(); + }); + test('should handle AvroSchema value of type', () => { + const av = new AvroSchema(); + av.name = 'test'; + av.type = { name: 'test1', type: 'int' }; + + const model = AvroToMetaModel(av); + + expect(model).not.toBeUndefined(); + expect(model instanceof IntegerModel).toBeTruthy(); + }); }); diff --git a/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap b/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap index db0fd50506..31f4cadd6d 100644 --- a/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap +++ b/test/processors/__snapshots__/AvroSchemaInputProcessor.spec.ts.snap @@ -11,18 +11,42 @@ InputMetaModel { "originalInput": undefined, "properties": Object { "address": ObjectPropertyModel { - "property": AnyModel { - "name": "address", + "property": ObjectModel { + "name": "Address", "options": Object { "isNullable": false, }, "originalInput": undefined, + "properties": Object { + "country": ObjectPropertyModel { + "property": StringModel { + "name": "country", + "options": Object { + "isNullable": true, + }, + "originalInput": undefined, + }, + "propertyName": "country", + "required": true, + }, + "zipcode": ObjectPropertyModel { + "property": IntegerModel { + "name": "zipcode", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, + "propertyName": "zipcode", + "required": true, + }, + }, }, "propertyName": "address", "required": true, }, "age": ObjectPropertyModel { - "property": AnyModel { + "property": IntegerModel { "name": "age", "options": Object { "isNullable": true, @@ -33,18 +57,25 @@ InputMetaModel { "required": true, }, "certifications": ObjectPropertyModel { - "property": AnyModel { - "name": "certifications", + "property": ArrayModel { + "name": "undefined", "options": Object { "isNullable": false, }, "originalInput": undefined, + "valueModel": StringModel { + "name": "undefined_string", + "options": Object { + "isNullable": false, + }, + "originalInput": undefined, + }, }, "propertyName": "certifications", "required": true, }, "email": ObjectPropertyModel { - "property": AnyModel { + "property": StringModel { "name": "email", "options": Object { "isNullable": true, @@ -55,12 +86,34 @@ InputMetaModel { "required": true, }, "favoriteProgrammingLanguage": ObjectPropertyModel { - "property": AnyModel { - "name": "favoriteProgrammingLanguage", + "property": EnumModel { + "name": "ProgrammingLanguage", "options": Object { "isNullable": false, }, "originalInput": undefined, + "values": Array [ + EnumValueModel { + "key": "JS", + "value": "JS", + }, + EnumValueModel { + "key": "Java", + "value": "Java", + }, + EnumValueModel { + "key": "Go", + "value": "Go", + }, + EnumValueModel { + "key": "Rust", + "value": "Rust", + }, + EnumValueModel { + "key": "C", + "value": "C", + }, + ], }, "propertyName": "favoriteProgrammingLanguage", "required": true, From 3db2eb2e61fa1d7b7c0138ff2141e85eaf9e08fe Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Tue, 9 Apr 2024 23:19:22 +0530 Subject: [PATCH 08/13] chore: resolve suggestions --- src/processors/AvroSchemaInputProcessor.ts | 1 - src/processors/InputProcessor.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/processors/AvroSchemaInputProcessor.ts b/src/processors/AvroSchemaInputProcessor.ts index 8c41257cb5..2dcb3ba329 100644 --- a/src/processors/AvroSchemaInputProcessor.ts +++ b/src/processors/AvroSchemaInputProcessor.ts @@ -27,7 +27,6 @@ export class AvroSchemaInputProcessor extends AbstractInputProcessor { * * @param input */ - shouldProcess(input?: any): boolean { if ( input === '' || diff --git a/src/processors/InputProcessor.ts b/src/processors/InputProcessor.ts index 6a63fce83c..6c15aa4aca 100644 --- a/src/processors/InputProcessor.ts +++ b/src/processors/InputProcessor.ts @@ -20,7 +20,7 @@ export class InputProcessor { this.setProcessor('openapi', new OpenAPIInputProcessor()); this.setProcessor('default', new JsonSchemaInputProcessor()); this.setProcessor('typescript', new TypeScriptInputProcessor()); - this.setProcessor('avroSchema', new AvroSchemaInputProcessor()); + this.setProcessor('avro', new AvroSchemaInputProcessor()); } /** From 017d635654881d5f80987fd4d4c43fa7ca7ea917 Mon Sep 17 00:00:00 2001 From: akkshitgupta <96991785+akkshitgupta@users.noreply.github.com> Date: Sat, 13 Apr 2024 01:29:01 +0530 Subject: [PATCH 09/13] docs: write docs and example for Avro Schema input format --- README.md | 4 + docs/usage.md | 11 +++ examples/README.md | 1 + examples/avro-schema-from-object/README.md | 17 ++++ .../__snapshots__/index.spec.ts.snap | 95 +++++++++++++++++++ .../avro-schema-from-object/index.spec.ts | 15 +++ examples/avro-schema-from-object/index.ts | 81 ++++++++++++++++ .../avro-schema-from-object/package-lock.json | 10 ++ examples/avro-schema-from-object/package.json | 12 +++ 9 files changed, 246 insertions(+) create mode 100644 examples/avro-schema-from-object/README.md create mode 100644 examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap create mode 100644 examples/avro-schema-from-object/index.spec.ts create mode 100644 examples/avro-schema-from-object/index.ts create mode 100644 examples/avro-schema-from-object/package-lock.json create mode 100644 examples/avro-schema-from-object/package.json diff --git a/README.md b/README.md index c24fe66d00..42336e34b5 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,10 @@ The following table provides a short summary of available features for supported AsyncAPI We support the following AsyncAPI versions: 2.0.0 -> 2.6.0, which generates models for all the defined message payloads. It supports the following schemaFormats AsyncAPI Schema object, JSON Schema draft 7, AVRO 1.9, RAML 1.0 data type, and OpenAPI 3.0 Schema. + + Avro Schema + we support the Avro Schema version 1.11.1 to generate models + JSON Schema We support the following JSON Schema versions: Draft-4, Draft-6 and Draft-7 diff --git a/docs/usage.md b/docs/usage.md index 61ac2f3a0f..cca1b2d101 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -15,6 +15,7 @@ For more specific integration options, please check out the [integration documen - [Generate models from AsyncAPI documents](#generate-models-from-asyncapi-documents) * [Limitations and Compatibility](#limitations-and-compatibility) + [Polymorphism](#polymorphism) +- [Generate models from Avro Schema documents](#generate-models-from-avro-schema-documents) - [Generate models from JSON Schema documents](#generate-models-from-json-schema-documents) - [Generate models from Swagger 2.0 documents](#generate-models-from-swagger-20-documents) * [Limitations and Compatibility](#limitations-and-compatibility-1) @@ -107,6 +108,16 @@ There are three ways to generate models for a JSON Schema document. The library expects the `$schema` property for the document to be set in order to understand the input format. By default, if no other inputs are detected, it defaults to `JSON Schema draft 7`. The process of interpreting a JSON Schema to a model can be read [here](./inputs/JSON_Schema.md). +## Generate models from Avro Schema documents + +See the below example to get started with Avro Schema for generating models. + +- [Generate from an Avro Schema JS Object](../examples/avro-schema-from-object) + +The Avro input processor expects the `type` property, as per [Avro Schema specs](https://avro.apache.org/docs/1.11.1/specification/#schema-declaration), in the input object in order to proceed successfully. + +> Note: Currently, we support `record` datatype for generating the `Object Model`. + ## Generate models from Swagger 2.0 documents There are one way to generate models from a Swagger 2.0 document. diff --git a/examples/README.md b/examples/README.md index 332852ece6..89a6344143 100644 --- a/examples/README.md +++ b/examples/README.md @@ -43,6 +43,7 @@ These examples show a specific input and how they can be used: - [asyncapi-raml-schema](./asyncapi-raml-schema) - A basic example of how to use Modelina with an AsyncAPI document using RAML 1.0 data types as payload format. - [asyncapi-from-parser](./asyncapi-from-parser) - A basic example where an AsyncAPI JS object from the [parser-js](https://github.com/asyncapi/parser-js) is used to generate models. - [asyncapi-from-v1-parser](./asyncapi-from-v1-parser) - A basic example where an AsyncAPI JS object from the old v1 [parser-js](https://github.com/asyncapi/parser-js) is used to generate models. +- [avro-schema-from-object](./avro-schema-from-object) - A basic example where an Avro Schema JS Object is used to generate models. - [json-schema-draft7-from-object](./json-schema-draft7-from-object) - A basic example where a JSON Schema draft 7 JS object is used to generate models. - [json-schema-draft6-from-object](./json-schema-draft6-from-object) - A basic example where a JSON Schema draft 6 JS object is used to generate models. - [json-schema-draft4-from-object](./json-schema-draft4-from-object) - A basic example where a JSON Schema draft 4 JS object is used to generate models. diff --git a/examples/avro-schema-from-object/README.md b/examples/avro-schema-from-object/README.md new file mode 100644 index 0000000000..5d9185a34c --- /dev/null +++ b/examples/avro-schema-from-object/README.md @@ -0,0 +1,17 @@ +# Avro Schema Input + +A basic example of how to use Modelina with an Avro Schema JS object to generate models. + +## How to run this example + +Run this example using: + +```sh +npm i && npm run start +``` + +If you are on Windows, use the `start:windows` script instead: + +```sh +npm i && npm run start:windows +``` diff --git a/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000..f3bd4a23da --- /dev/null +++ b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should be able to render model using Avro Schema and should log expected output to console 1`] = ` +class Person { + private _reservedName: string; + private _serialNo: string; + private _email: string | null; + private _age: number | null; + private _favoriteProgrammingLanguage: ProgrammingLanguage; + private _certifications: string[]; + private _address: Address; + private _weight: number; + private _height: number; + private _someid: string; + + constructor(input: { + reservedName: string, + serialNo: string, + email: string | null, + age: number | null, + favoriteProgrammingLanguage: ProgrammingLanguage, + certifications: string[], + address: Address, + weight: number, + height: number, + someid: string, + }) { + this._reservedName = input.reservedName; + this._serialNo = input.serialNo; + this._email = input.email; + this._age = input.age; + this._favoriteProgrammingLanguage = input.favoriteProgrammingLanguage; + this._certifications = input.certifications; + this._address = input.address; + this._weight = input.weight; + this._height = input.height; + this._someid = input.someid; + } + + get reservedName(): string { return this._reservedName; } + set reservedName(reservedName: string) { this._reservedName = reservedName; } + + get serialNo(): string { return this._serialNo; } + set serialNo(serialNo: string) { this._serialNo = serialNo; } + + get email(): string | null { return this._email; } + set email(email: string | null) { this._email = email; } + + get age(): number | null { return this._age; } + set age(age: number | null) { this._age = age; } + + get favoriteProgrammingLanguage(): ProgrammingLanguage { return this._favoriteProgrammingLanguage; } + set favoriteProgrammingLanguage(favoriteProgrammingLanguage: ProgrammingLanguage) { this._favoriteProgrammingLanguage = favoriteProgrammingLanguage; } + + get certifications(): string[] { return this._certifications; } + set certifications(certifications: string[]) { this._certifications = certifications; } + + get address(): Address { return this._address; } + set address(address: Address) { this._address = address; } + + get weight(): number { return this._weight; } + set weight(weight: number) { this._weight = weight; } + + get height(): number { return this._height; } + set height(height: number) { this._height = height; } + + get someid(): string { return this._someid; } + set someid(someid: string) { this._someid = someid; } +} +enum ProgrammingLanguage { + JS = "JS", + RESERVED_JAVA = "Java", + GO = "Go", + RUST = "Rust", + C = "C", +} +class Address { + private _zipcode: number; + private _country: string | null; + + constructor(input: { + zipcode: number, + country: string | null, + }) { + this._zipcode = input.zipcode; + this._country = input.country; + } + + get zipcode(): number { return this._zipcode; } + set zipcode(zipcode: number) { this._zipcode = zipcode; } + + get country(): string | null { return this._country; } + set country(country: string | null) { this._country = country; } +} +` \ No newline at end of file diff --git a/examples/avro-schema-from-object/index.spec.ts b/examples/avro-schema-from-object/index.spec.ts new file mode 100644 index 0000000000..514eda6705 --- /dev/null +++ b/examples/avro-schema-from-object/index.spec.ts @@ -0,0 +1,15 @@ +const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { + return; +}); +import { generate } from './index'; + +describe('Should be able to render model using Avro Schema', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + test('and should log expected output to console', async () => { + await generate(); + expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls[0]).toMatchSnapshot(); + }); +}); diff --git a/examples/avro-schema-from-object/index.ts b/examples/avro-schema-from-object/index.ts new file mode 100644 index 0000000000..83b45c137a --- /dev/null +++ b/examples/avro-schema-from-object/index.ts @@ -0,0 +1,81 @@ +import { TypeScriptGenerator } from '../../src'; + +const generator = new TypeScriptGenerator(); +const AvroSchemaDoc = { + name: 'Person', + namespace: 'com.company', + type: 'record', + fields: [ + { name: 'name', type: 'string', example: 'Donkey', minLength: 0 }, + { name: 'serialNo', type: 'string', minLength: 0, maxLength: 50 }, + { + name: 'email', + type: ['null', 'string'], + example: 'donkey@asyncapi.com', + pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' + }, + { + name: 'age', + type: ['null', 'int'], + default: null, + example: 123, + exclusiveMinimum: 0, + exclusiveMaximum: 200 + }, + { + name: 'favoriteProgrammingLanguage', + type: { + name: 'ProgrammingLanguage', + type: 'enum', + symbols: ['JS', 'Java', 'Go', 'Rust', 'C'], + default: 'JS' + } + }, + { + name: 'certifications', + type: { + type: 'array', + items: 'string', + minItems: 1, + maxItems: 500, + uniqueItems: true + } + }, + { + name: 'address', + type: { + name: 'Address', + type: 'record', + fields: [ + { name: 'zipcode', type: 'int', example: 53003 }, + { name: 'country', type: ['null', 'string'] } + ] + } + }, + { + name: 'weight', + type: 'float', + example: 65.1, + minimum: 0, + maximum: 500 + }, + { + name: 'height', + type: 'double', + example: 1.85, + minimum: 0, + maximum: 3.0 + }, + { name: 'someid', type: 'string', logicalType: 'uuid' } + ] +}; + +export async function generate(): Promise { + const models = await generator.generate(AvroSchemaDoc); + for (const model of models) { + console.log(model.result); + } +} +if (require.main === module) { + generate(); +} diff --git a/examples/avro-schema-from-object/package-lock.json b/examples/avro-schema-from-object/package-lock.json new file mode 100644 index 0000000000..d7ba6c8425 --- /dev/null +++ b/examples/avro-schema-from-object/package-lock.json @@ -0,0 +1,10 @@ +{ + "name": "avro-to-models", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "hasInstallScript": true + } + } +} diff --git a/examples/avro-schema-from-object/package.json b/examples/avro-schema-from-object/package.json new file mode 100644 index 0000000000..56f1490f97 --- /dev/null +++ b/examples/avro-schema-from-object/package.json @@ -0,0 +1,12 @@ +{ + "config": { + "example_name": "avro-schema-from-object" + }, + "scripts": { + "install": "cd ../.. && npm i", + "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts", + "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts", + "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts", + "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts" + } +} From ba51c4526c57aa9342b7789b49dc40fe623e4715 Mon Sep 17 00:00:00 2001 From: Akshit Gupta <96991785+akkshitgupta@users.noreply.github.com> Date: Fri, 12 Apr 2024 21:20:27 +0000 Subject: [PATCH 10/13] test: update mock calls length --- .../__snapshots__/index.spec.ts.snap | 33 +++---------------- .../avro-schema-from-object/index.spec.ts | 2 +- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap index f3bd4a23da..fb865df00e 100644 --- a/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap +++ b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap @@ -1,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Should be able to render model using Avro Schema and should log expected output to console 1`] = ` -class Person { +Array [ + "class Person { private _reservedName: string; private _serialNo: string; private _email: string | null; @@ -66,30 +67,6 @@ class Person { get someid(): string { return this._someid; } set someid(someid: string) { this._someid = someid; } -} -enum ProgrammingLanguage { - JS = "JS", - RESERVED_JAVA = "Java", - GO = "Go", - RUST = "Rust", - C = "C", -} -class Address { - private _zipcode: number; - private _country: string | null; - - constructor(input: { - zipcode: number, - country: string | null, - }) { - this._zipcode = input.zipcode; - this._country = input.country; - } - - get zipcode(): number { return this._zipcode; } - set zipcode(zipcode: number) { this._zipcode = zipcode; } - - get country(): string | null { return this._country; } - set country(country: string | null) { this._country = country; } -} -` \ No newline at end of file +}", +] +`; diff --git a/examples/avro-schema-from-object/index.spec.ts b/examples/avro-schema-from-object/index.spec.ts index 514eda6705..7b51af48a6 100644 --- a/examples/avro-schema-from-object/index.spec.ts +++ b/examples/avro-schema-from-object/index.spec.ts @@ -9,7 +9,7 @@ describe('Should be able to render model using Avro Schema', () => { }); test('and should log expected output to console', async () => { await generate(); - expect(spy.mock.calls.length).toEqual(1); + expect(spy.mock.calls.length).toEqual(3); expect(spy.mock.calls[0]).toMatchSnapshot(); }); }); From bdb1dd57e21b403fa8f81e842e3e234c92cd7b31 Mon Sep 17 00:00:00 2001 From: Akshit Gupta <96991785+akkshitgupta@users.noreply.github.com> Date: Sat, 13 Apr 2024 06:47:02 +0000 Subject: [PATCH 11/13] fix: typo resulting to fail test --- test/processors/InputProcessor.spec.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/processors/InputProcessor.spec.ts b/test/processors/InputProcessor.spec.ts index cc974a3b7f..367786c517 100644 --- a/test/processors/InputProcessor.spec.ts +++ b/test/processors/InputProcessor.spec.ts @@ -74,7 +74,7 @@ describe('InputProcessor', () => { const processor = new InputProcessor(); processor.setProcessor('asyncapi', asyncInputProcessor); processor.setProcessor('swagger', swaggerInputProcessor); - processor.setProcessor('avroSchema', avroSchemaInputProcessor); + processor.setProcessor('avro', avroSchemaInputProcessor); processor.setProcessor('openapi', openAPIInputProcessor); processor.setProcessor('default', defaultInputProcessor); return { @@ -187,25 +187,22 @@ describe('InputProcessor', () => { const { processor, avroSchemaInputProcessor, defaultInputProcessor } = getProcessors(); const inputSchemaString = fs.readFileSync( - path.resolve(__dirname, './AvroSchemaInputProcessor/sample.json'), + path.resolve(__dirname, './AvroSchemaInputProcessor/basic.json'), 'utf8' ); const inputSchema = JSON.parse(inputSchemaString); await processor.process(inputSchema); - expect(avroSchemaInputProcessor.process).not.toHaveBeenCalled(); - expect(avroSchemaInputProcessor.shouldProcess).toHaveBeenNthCalledWith( - 1, - inputSchema - ); - expect(defaultInputProcessor.process).toHaveBeenNthCalledWith( + expect(avroSchemaInputProcessor.process).toHaveBeenNthCalledWith( 1, inputSchema, undefined ); - expect(defaultInputProcessor.shouldProcess).toHaveBeenNthCalledWith( + expect(avroSchemaInputProcessor.shouldProcess).toHaveBeenNthCalledWith( 1, inputSchema ); + expect(defaultInputProcessor.process).not.toHaveBeenCalled(); + expect(defaultInputProcessor.shouldProcess).not.toHaveBeenCalled(); }); test('should be able to process AsyncAPI schema input with options', async () => { const { processor, asyncInputProcessor, defaultInputProcessor } = From e41e4d61a2db94ed67a112456f38e311cad049f1 Mon Sep 17 00:00:00 2001 From: Akshit Gupta <96991785+akkshitgupta@users.noreply.github.com> Date: Sun, 14 Apr 2024 08:56:12 +0000 Subject: [PATCH 12/13] fix: example and readme --- README.md | 2 +- .../__snapshots__/index.spec.ts.snap | 54 --------------- .../avro-schema-from-object/index.spec.ts | 2 +- examples/avro-schema-from-object/index.ts | 66 ++----------------- 4 files changed, 8 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 568ac640e5..01f0efbb16 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ The following table provides a short summary of available features for supported Avro Schema - we support the Avro Schema version 1.11.1 to generate models + We support the following Avro versions: v1.x JSON Schema diff --git a/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap index fb865df00e..9a10481a44 100644 --- a/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap +++ b/examples/avro-schema-from-object/__snapshots__/index.spec.ts.snap @@ -4,69 +4,15 @@ exports[`Should be able to render model using Avro Schema and should log expecte Array [ "class Person { private _reservedName: string; - private _serialNo: string; - private _email: string | null; - private _age: number | null; - private _favoriteProgrammingLanguage: ProgrammingLanguage; - private _certifications: string[]; - private _address: Address; - private _weight: number; - private _height: number; - private _someid: string; constructor(input: { reservedName: string, - serialNo: string, - email: string | null, - age: number | null, - favoriteProgrammingLanguage: ProgrammingLanguage, - certifications: string[], - address: Address, - weight: number, - height: number, - someid: string, }) { this._reservedName = input.reservedName; - this._serialNo = input.serialNo; - this._email = input.email; - this._age = input.age; - this._favoriteProgrammingLanguage = input.favoriteProgrammingLanguage; - this._certifications = input.certifications; - this._address = input.address; - this._weight = input.weight; - this._height = input.height; - this._someid = input.someid; } get reservedName(): string { return this._reservedName; } set reservedName(reservedName: string) { this._reservedName = reservedName; } - - get serialNo(): string { return this._serialNo; } - set serialNo(serialNo: string) { this._serialNo = serialNo; } - - get email(): string | null { return this._email; } - set email(email: string | null) { this._email = email; } - - get age(): number | null { return this._age; } - set age(age: number | null) { this._age = age; } - - get favoriteProgrammingLanguage(): ProgrammingLanguage { return this._favoriteProgrammingLanguage; } - set favoriteProgrammingLanguage(favoriteProgrammingLanguage: ProgrammingLanguage) { this._favoriteProgrammingLanguage = favoriteProgrammingLanguage; } - - get certifications(): string[] { return this._certifications; } - set certifications(certifications: string[]) { this._certifications = certifications; } - - get address(): Address { return this._address; } - set address(address: Address) { this._address = address; } - - get weight(): number { return this._weight; } - set weight(weight: number) { this._weight = weight; } - - get height(): number { return this._height; } - set height(height: number) { this._height = height; } - - get someid(): string { return this._someid; } - set someid(someid: string) { this._someid = someid; } }", ] `; diff --git a/examples/avro-schema-from-object/index.spec.ts b/examples/avro-schema-from-object/index.spec.ts index 7b51af48a6..514eda6705 100644 --- a/examples/avro-schema-from-object/index.spec.ts +++ b/examples/avro-schema-from-object/index.spec.ts @@ -9,7 +9,7 @@ describe('Should be able to render model using Avro Schema', () => { }); test('and should log expected output to console', async () => { await generate(); - expect(spy.mock.calls.length).toEqual(3); + expect(spy.mock.calls.length).toEqual(1); expect(spy.mock.calls[0]).toMatchSnapshot(); }); }); diff --git a/examples/avro-schema-from-object/index.ts b/examples/avro-schema-from-object/index.ts index 83b45c137a..2a07a0b10f 100644 --- a/examples/avro-schema-from-object/index.ts +++ b/examples/avro-schema-from-object/index.ts @@ -6,67 +6,13 @@ const AvroSchemaDoc = { namespace: 'com.company', type: 'record', fields: [ - { name: 'name', type: 'string', example: 'Donkey', minLength: 0 }, - { name: 'serialNo', type: 'string', minLength: 0, maxLength: 50 }, { - name: 'email', - type: ['null', 'string'], - example: 'donkey@asyncapi.com', - pattern: '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$' - }, - { - name: 'age', - type: ['null', 'int'], - default: null, - example: 123, - exclusiveMinimum: 0, - exclusiveMaximum: 200 - }, - { - name: 'favoriteProgrammingLanguage', - type: { - name: 'ProgrammingLanguage', - type: 'enum', - symbols: ['JS', 'Java', 'Go', 'Rust', 'C'], - default: 'JS' - } - }, - { - name: 'certifications', - type: { - type: 'array', - items: 'string', - minItems: 1, - maxItems: 500, - uniqueItems: true - } - }, - { - name: 'address', - type: { - name: 'Address', - type: 'record', - fields: [ - { name: 'zipcode', type: 'int', example: 53003 }, - { name: 'country', type: ['null', 'string'] } - ] - } - }, - { - name: 'weight', - type: 'float', - example: 65.1, - minimum: 0, - maximum: 500 - }, - { - name: 'height', - type: 'double', - example: 1.85, - minimum: 0, - maximum: 3.0 - }, - { name: 'someid', type: 'string', logicalType: 'uuid' } + name: 'name', + type: 'string', + example: 'Donkey', + minLength: 0, + maxLenght: 20 + } ] }; From 0f78593bfaf00d6d79a2051000c2db745ffb46b9 Mon Sep 17 00:00:00 2001 From: Akshit Gupta <96991785+akkshitgupta@users.noreply.github.com> Date: Thu, 25 Apr 2024 12:38:34 +0000 Subject: [PATCH 13/13] chore: improve note and remove unnecessary code --- docs/usage.md | 4 ++-- src/models/AvroSchema.ts | 51 ---------------------------------------- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index cca1b2d101..60bf5eb701 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -114,9 +114,9 @@ See the below example to get started with Avro Schema for generating models. - [Generate from an Avro Schema JS Object](../examples/avro-schema-from-object) -The Avro input processor expects the `type` property, as per [Avro Schema specs](https://avro.apache.org/docs/1.11.1/specification/#schema-declaration), in the input object in order to proceed successfully. +The Avro input processor expects the `name` and `type` property, as per [Avro Schema specs](https://avro.apache.org/docs/1.11.1/specification/#schema-declaration), in the input object in order to proceed successfully. -> Note: Currently, we support `record` datatype for generating the `Object Model`. +> Note: Currently, we do not have a support for `map`, `fixed` and `byte` data type. It would be introduced soon. ## Generate models from Swagger 2.0 documents diff --git a/src/models/AvroSchema.ts b/src/models/AvroSchema.ts index 9b3fb0f83b..1d1dfbcb0a 100644 --- a/src/models/AvroSchema.ts +++ b/src/models/AvroSchema.ts @@ -22,55 +22,4 @@ export class AvroSchema { exclusiveMinimum?: unknown; exclusiveMaximum?: unknown; logicalType?: unknown; - - /** - * Takes a deep copy of the input object and converts it to an instance of AvroSchema. - * - * @param object - */ - static toSchema(object: Record): AvroSchema { - const convertedSchema = AvroSchema.internalToSchema(object); - if (convertedSchema instanceof AvroSchema) { - return convertedSchema; - } - throw new Error('Could not convert input to expected copy of AvroSchema'); - } - - private static internalToSchema( - object: any, - seenSchemas: Map = new Map() - ): any { - // if primitive types return as is - if (null === object || 'object' !== typeof object) { - return object; - } - - if (seenSchemas.has(object)) { - return seenSchemas.get(object) as any; - } - - if (object instanceof Array) { - const copy: any = []; - for (let i = 0, len = object.length; i < len; i++) { - copy[Number(i)] = AvroSchema.internalToSchema( - object[Number(i)], - seenSchemas - ); - } - return copy; - } - //Nothing else left then to create an object - const schema = new AvroSchema(); - seenSchemas.set(object, schema); - for (const [propName, prop] of Object.entries(object)) { - let copyProp = prop; - - // Ignore value properties (those with `any` type) as they should be saved as is regardless of value - if (propName !== 'default' && propName !== 'enum') { - copyProp = AvroSchema.internalToSchema(prop, seenSchemas); - } - (schema as any)[String(propName)] = copyProp; - } - return schema; - } }