From 903dafd09dbf2ef35bc090607ffe18cc659d2563 Mon Sep 17 00:00:00 2001 From: Dmitry Ostrikov Date: Tue, 13 Feb 2024 09:51:32 +0400 Subject: [PATCH] feat(oas): reduce complexity, increase coverage closes #224 --- packages/oas/src/converter/Sampler.ts | 58 ++-- .../src/traverse/DefaultTraverse.ts | 80 ++++-- .../openapi-sampler/tests/example.spec.ts | 249 ++++++++++++++++++ 3 files changed, 331 insertions(+), 56 deletions(-) diff --git a/packages/oas/src/converter/Sampler.ts b/packages/oas/src/converter/Sampler.ts index fbcd789c..f703651a 100644 --- a/packages/oas/src/converter/Sampler.ts +++ b/packages/oas/src/converter/Sampler.ts @@ -14,35 +14,15 @@ export class Sampler { tokens: string[]; } ): any { - return this.sample( - 'schema' in param - ? { - ...param.schema, - ...(param[VendorExtensions.X_EXAMPLE] !== undefined - ? { - [VendorExtensions.X_EXAMPLE]: - param[VendorExtensions.X_EXAMPLE] - } - : {}), - ...(param[VendorExtensions.X_EXAMPLES] !== undefined - ? { - [VendorExtensions.X_EXAMPLES]: - param[VendorExtensions.X_EXAMPLES] - } - : {}), - ...(param.example !== undefined ? { example: param.example } : {}) - } - : param, - { - spec: context.spec, - jsonPointer: pointer.compile([ - ...context.tokens, - 'parameters', - context.idx.toString(), - ...('schema' in param ? ['schema'] : []) - ]) - } - ); + return this.sample(this.createSchema(param), { + spec: context.spec, + jsonPointer: pointer.compile([ + ...context.tokens, + 'parameters', + context.idx.toString(), + ...('schema' in param ? ['schema'] : []) + ]) + }); } /** @@ -66,4 +46,24 @@ export class Sampler { throw new ConvertError(e.message, context?.jsonPointer); } } + + private createSchema(param: OpenAPI.Parameter): Schema { + return 'schema' in param + ? { + ...param.schema, + ...(param[VendorExtensions.X_EXAMPLE] !== undefined + ? { + [VendorExtensions.X_EXAMPLE]: param[VendorExtensions.X_EXAMPLE] + } + : {}), + ...(param[VendorExtensions.X_EXAMPLES] !== undefined + ? { + [VendorExtensions.X_EXAMPLES]: + param[VendorExtensions.X_EXAMPLES] + } + : {}), + ...(param.example !== undefined ? { example: param.example } : {}) + } + : param; + } } diff --git a/packages/openapi-sampler/src/traverse/DefaultTraverse.ts b/packages/openapi-sampler/src/traverse/DefaultTraverse.ts index dbaccbdc..73421460 100644 --- a/packages/openapi-sampler/src/traverse/DefaultTraverse.ts +++ b/packages/openapi-sampler/src/traverse/DefaultTraverse.ts @@ -345,70 +345,96 @@ export class DefaultTraverse implements Traverse { const example = schema[VendorExtensions.X_EXAMPLE] ?? schema[VendorExtensions.X_EXAMPLES]; - const schemaKeys = this.findSchemaKeys(schema); + const matchingSchema = this.getMatchingSchema(schema); + const isPrimitiveType = 0 === matchingSchema.keys.length; - if ( - !example || - typeof example !== 'object' || - 0 === schemaKeys.keys.length - ) { + if (!example || typeof example !== 'object' || isPrimitiveType) { return example; } - return this.matchVendorExampleKeys(example, schemaKeys); + return this.matchVendorExample(example, matchingSchema); } - private findSchemaKeys( + private getMatchingSchema( schema: Schema, depth: number = 0 ): { depth: number; keys: string[] } { if ('items' in schema) { - return this.findSchemaKeys(schema.items, 1 + depth); + return this.getMatchingSchema(schema.items, 1 + depth); } return { depth, - keys: 'properties' in schema ? Object.keys(schema.properties) : [] + keys: + 'properties' in schema && schema.properties + ? Object.keys(schema.properties) + : [] }; } - private matchVendorExampleKeys( + private matchVendorExample( example: unknown, - schemaKeys: { depth: number; keys: string[] }, - possibleExamples: unknown[] = [] + matchingSchema: { depth: number; keys: string[] }, + possibleExample: unknown[] = [] ): unknown { if (!example || typeof example !== 'object') { return undefined; } - if (schemaKeys.depth > 0 && Array.isArray(example)) { - return this.matchVendorExampleKeys( + if (matchingSchema.depth > 0 && Array.isArray(example)) { + return this.matchArrayVendorExample( + example, + matchingSchema, + possibleExample + ); + } + + return this.matchObjectVendorExample( + example, + matchingSchema, + possibleExample + ); + } + + private matchArrayVendorExample( + example: unknown, + matchingSchema: { depth: number; keys: string[] }, + possibleExample: unknown[] + ): unknown { + if (matchingSchema.depth > 0 && Array.isArray(example)) { + possibleExample.push(example); + + return this.matchArrayVendorExample( [...example, undefined].shift(), { - ...schemaKeys, - depth: schemaKeys.depth - 1 + ...matchingSchema, + depth: matchingSchema.depth - 1 }, - [...possibleExamples, example] + possibleExample ); } - const objectKeys = Object.keys(example); + return !!example && Array.isArray(example) + ? undefined + : this.matchObjectVendorExample(example, matchingSchema, possibleExample); + } - if (objectKeys.every((key) => schemaKeys.keys.includes(key))) { - if (possibleExamples.length > 0) { - return possibleExamples.shift(); - } + private matchObjectVendorExample( + example: unknown, + matchingSchema: { depth: number; keys: string[] }, + possibleExample: unknown[] + ): unknown { + const objectKeys = Object.keys(example ?? {}); - return example; + if (objectKeys.every((key) => matchingSchema.keys.includes(key))) { + return possibleExample.length > 0 ? possibleExample.shift() : example; } for (const key of objectKeys) { - const value = this.matchVendorExampleKeys(example[key], schemaKeys); + const value = this.matchVendorExample(example[key], matchingSchema); if (value) { return value; } } - - return undefined; } } diff --git a/packages/openapi-sampler/tests/example.spec.ts b/packages/openapi-sampler/tests/example.spec.ts index e300bda6..06e6f89b 100644 --- a/packages/openapi-sampler/tests/example.spec.ts +++ b/packages/openapi-sampler/tests/example.spec.ts @@ -99,4 +99,253 @@ describe('Example', () => { // assert expect(result).toEqual('foo'); }); + + it.each([ + { + [VendorExtensions.X_EXAMPLE]: { + name: 'name', + age: 30 + } + }, + { + [VendorExtensions.X_EXAMPLE]: { + 'application/json': { + name: 'name', + age: 30 + } + } + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': { + name: 'name', + age: 30 + } + } + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': { + some: { + name: 'name', + age: 30 + } + } + } + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': { + some: { + name: 'name', + age: 30 + } + } + } + }, + { + [VendorExtensions.X_EXAMPLES]: [ + { + name: 'name', + age: 30 + } + ] + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': [ + { + name: 'name', + age: 30 + } + ] + } + } + ])( + 'should match %j object vendor example when includeVendorExamples is true', + (input) => { + // arrange + const expected = { + name: 'name', + age: 30 + }; + + const schema = { + type: 'object', + properties: { + name: { + type: 'string' + }, + age: { + type: 'integer' + } + }, + ...input + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toMatchObject(expected); + } + ); + + it.each(['some-string', { name: 'some-name', points: 100 }])( + 'should ignore %j object vendor example when includeVendorExamples is true', + (input) => { + // arrange + const schema = { + type: 'object', + properties: { + name: { + type: 'string' + }, + age: { + type: 'integer' + } + }, + [VendorExtensions.X_EXAMPLES]: { + 'some-example': input + } + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toMatchObject({ age: 42, name: 'lorem' }); + } + ); + + it.each([ + [ + { + [VendorExtensions.X_EXAMPLE]: [ + { + name: 'name', + age: 30 + } + ] + }, + { + [VendorExtensions.X_EXAMPLE]: { + 'application/json': [ + { + name: 'name', + age: 30 + } + ] + } + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': [ + { + name: 'name', + age: 30 + } + ] + } + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': [ + { + some: { + name: 'name', + age: 30 + } + } + ] + } + }, + { + [VendorExtensions.X_EXAMPLES]: [ + { + name: 'name', + age: 30 + } + ] + }, + { + [VendorExtensions.X_EXAMPLES]: { + 'application/json': [ + { + name: 'name', + age: 30 + } + ] + } + } + ] + ])( + 'should match %j array vendor example when includeVendorExamples is true', + (input) => { + // arrange + const expected = [ + { + name: 'name', + age: 30 + } + ]; + + const schema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + age: { + type: 'integer' + } + } + }, + ...input + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toMatchObject(expected); + } + ); + + it.each([ + [[{ name: 'some-name', age: 30 }]], + 2, + 'some-string', + { name: 'some-name', points: 100 } + ])( + 'should ignore %j array vendor example when includeVendorExamples is true', + (input) => { + // arrange + const schema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + age: { + type: 'integer' + } + } + }, + [VendorExtensions.X_EXAMPLES]: { + 'some-example': input + } + }; + + // act + const result = sample(schema, { includeVendorExamples: true }); + + // assert + expect(result).toMatchObject([{ age: 42, name: 'lorem' }]); + } + ); });