diff --git a/packages/http/src/mocker/__tests__/HttpMocker.spec.ts b/packages/http/src/mocker/__tests__/HttpMocker.spec.ts index 9ad103788..65a5d33f4 100644 --- a/packages/http/src/mocker/__tests__/HttpMocker.spec.ts +++ b/packages/http/src/mocker/__tests__/HttpMocker.spec.ts @@ -522,6 +522,24 @@ describe('mocker', () => { it('should return the example key', () => assertRight(eitherResponse, response => expect(response.body).toHaveProperty('name', 'Clark'))); + describe('when the property has anyOf and an examples value', () => { + const eitherResponseWithAnyOfExample = mockResponseWithSchema({ + type: 'object', + properties: { + title: { + anyOf: [{ type: 'string' }, { type: 'null' }], + title: 'Title', + examples: ['Some kind of title'], + }, + }, + }); + + it('should use the property example', () => + assertRight(eitherResponseWithAnyOfExample, response => + expect(response.body).toHaveProperty('title', 'Some kind of title') + )); + }); + describe('and also a default key', () => { const eitherResponseWithDefault = mockResponseWithSchema({ type: 'object', diff --git a/packages/http/src/mocker/generator/JSONSchema.ts b/packages/http/src/mocker/generator/JSONSchema.ts index dc0071a12..d404d5e57 100644 --- a/packages/http/src/mocker/generator/JSONSchema.ts +++ b/packages/http/src/mocker/generator/JSONSchema.ts @@ -77,10 +77,13 @@ export function generate( // @ts-ignore () => { if (seed) { - JSONSchemaFaker.option('random', seedrandom(seed)) + JSONSchemaFaker.option('random', seedrandom(seed)); } // @ts-ignore - return sortSchemaAlphabetically(JSONSchemaFaker.generate({ ...cloneDeep(updatedSource), __bundled__: bundle })) + return sortSchemaAlphabetically( + // @ts-ignore + JSONSchemaFaker.generate({ ...cloneDeep(updatedSource), __bundled__: bundle }) + ); }, toError ) @@ -111,8 +114,11 @@ export function sortSchemaAlphabetically(source: any): any { } export function generateStatic(operation: IHttpOperation, source: JSONSchema): Either { + const schemaWithPromotedUnionExamples = cloneDeep(source); + promoteUnionExamples(schemaWithPromotedUnionExamples); + return pipe( - tryCatch(() => sampler.sample(source, { ticks: 2500 }, operation), toError), + tryCatch(() => sampler.sample(schemaWithPromotedUnionExamples, { ticks: 2500 }, operation), toError), E.mapLeft(err => { if (err instanceof sampler.SchemaSizeExceededError) { return new SchemaTooComplexGeneratorError(operation, err); @@ -122,10 +128,61 @@ export function generateStatic(operation: IHttpOperation, source: JSONSchema): E ); } +function promoteUnionExamples(source: JSONSchema): void { + const schemaMaps = new Set(['properties', 'patternProperties', 'definitions', '$defs', 'dependentSchemas']); + const schemaKeywords = [ + ...schemaMaps, + 'additionalProperties', + 'allOf', + 'anyOf', + 'oneOf', + 'items', + 'not', + 'if', + 'then', + 'else', + 'contains', + 'propertyNames', + ]; + const visited = new WeakSet(); + + const visit = (schema: any): void => { + if (!schema || typeof schema !== 'object' || visited.has(schema)) return; + visited.add(schema); + + // The sampler traverses unions before checking examples on the union schema itself. + if ( + (schema.anyOf?.length || schema.oneOf?.length) && + Array.isArray(schema.examples) && + schema.examples.length > 0 && + schema.example === undefined + ) { + schema.example = schema.examples[0]; + } + + for (const keyword of schemaKeywords) { + const child = schema[keyword]; + if (!child || typeof child !== 'object') continue; + if (schemaMaps.has(keyword)) { + Object.values(child).forEach(visit); + } else if (Array.isArray(child)) { + child.forEach(visit); + } else { + visit(child); + } + } + }; + + visit(source); +} + export class GeneratorError extends Error {} export class SchemaTooComplexGeneratorError extends GeneratorError { - constructor(operation: IHttpOperation, public readonly cause: Error) { + constructor( + operation: IHttpOperation, + public readonly cause: Error + ) { super( `The operation ${operation.method.toUpperCase()} ${ operation.path