From a19721b52b6172c757badda0715f8d2118eacc6b Mon Sep 17 00:00:00 2001 From: Jon Ursenbach Date: Mon, 26 Jun 2023 16:37:47 -0700 Subject: [PATCH] fix: sample generation for mixed null schemas (#775) --- __tests__/samples/index.test.ts | 77 +++++++++++++++++++++++++++++---- src/samples/index.ts | 33 +++++++++++++- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/__tests__/samples/index.test.ts b/__tests__/samples/index.test.ts index a6108a50..9879b183 100644 --- a/__tests__/samples/index.test.ts +++ b/__tests__/samples/index.test.ts @@ -233,7 +233,7 @@ describe('sampleFromSchema', () => { expect(sampleFromSchema(definition)).toStrictEqual(expected); }); - it('should return an undefined value for type=file', () => { + it('should return an undefined value for a `file` type', () => { const definition: RMOAS.SchemaObject = { type: 'array', // @ts-expect-error We're testing the failure case for `file` not being a valid type. @@ -247,7 +247,68 @@ describe('sampleFromSchema', () => { expect(sampleFromSchema(definition)).toStrictEqual(expected); }); - describe('type=boolean', () => { + describe('`type: mixed`', () => { + it('returns a boolean for a `boolean` array type', () => { + const definition: RMOAS.SchemaObject = { + type: ['boolean'], + }; + + expect(sampleFromSchema(definition)).toBe(true); + }); + + it('returns a non-null example for a mixed type of two types', () => { + const definition: RMOAS.SchemaObject = { + type: ['null', 'boolean'], + }; + + expect(sampleFromSchema(definition)).toBe(true); + }); + + it('returns a non-null example for a mixed type of more than two types', () => { + const definition: RMOAS.SchemaObject = { + type: ['null', 'integer', 'boolean'], + }; + + // We're just picking whatever the first type we see and am generating an example for that. + expect(sampleFromSchema(definition)).toBe(0); + }); + + it('should be able to handle a mixed `null` and `object` type', () => { + const definition: RMOAS.SchemaObject = { + anyOf: [ + { + type: 'null', + }, + { + type: 'object', + properties: { + buster: { + type: 'string', + }, + }, + }, + ], + }; + + expect(sampleFromSchema(definition)).toStrictEqual({ + buster: 'string', + }); + }); + }); + + describe('`type: null`', () => { + it('returns a null for a null type', () => { + const definition: RMOAS.SchemaObject = { + type: 'null', + }; + + const expected = null; + + expect(sampleFromSchema(definition)).toStrictEqual(expected); + }); + }); + + describe('`type: boolean`', () => { it('returns a boolean for a boolean', () => { const definition: RMOAS.SchemaObject = { type: 'boolean', @@ -270,7 +331,7 @@ describe('sampleFromSchema', () => { }); }); - describe('type=number', () => { + describe('`type: number`', () => { it('returns a number for a number with no format', () => { const definition: RMOAS.SchemaObject = { type: 'number', @@ -304,7 +365,7 @@ describe('sampleFromSchema', () => { }); }); - describe('type=string', () => { + describe('`type: string`', () => { it('returns a date-time for a string with format=date-time', () => { const definition: RMOAS.SchemaObject = { type: 'string', @@ -394,8 +455,8 @@ describe('sampleFromSchema', () => { }); }); - describe('type=undefined', () => { - it('should handle if an object is present but is missing type=object', () => { + describe('mising `type`', () => { + it('should handle if an object is present but is missing `type: object`', () => { const definition: RMOAS.SchemaObject = { properties: { foo: { @@ -411,7 +472,7 @@ describe('sampleFromSchema', () => { expect(sampleFromSchema(definition)).toStrictEqual(expected); }); - it('should handle if an array is present but is missing type=array', () => { + it('should handle if an array is present but is missing `type: array`', () => { const definition: RMOAS.SchemaObject = { items: { type: 'string', @@ -452,7 +513,7 @@ describe('sampleFromSchema', () => { }); }); - describe('type=array', () => { + describe('`type: array`', () => { it('returns array with sample of array type', () => { const definition: RMOAS.SchemaObject = { type: 'array', diff --git a/src/samples/index.ts b/src/samples/index.ts index c77cb49d..aaf9ad86 100644 --- a/src/samples/index.ts +++ b/src/samples/index.ts @@ -34,7 +34,23 @@ const primitives: Record string | nu const primitive = (schema: RMOAS.SchemaObject) => { schema = objectify(schema); - const { type, format } = schema; + const { format } = schema; + let { type } = schema; + + if (type === 'null') { + return null; + } else if (Array.isArray(type)) { + if (type.length === 1) { + type = type[0]; + } else { + // If one of our types is `null` then we should generate a sample for the non-null value. + if (type.includes('null')) { + type = type.filter(t => t !== 'null'); + } + + type = type.shift(); + } + } // @todo add support for if `type` is an array const fn = primitives[`${type}_${format}`] || primitives[type as string]; @@ -86,7 +102,20 @@ function sampleFromSchema( return undefined; } } else if (hasPolymorphism) { - return sampleFromSchema((objectifySchema[hasPolymorphism] as RMOAS.SchemaObject[])[0], opts); + const samples = (objectifySchema[hasPolymorphism] as RMOAS.SchemaObject[]).map(s => { + return sampleFromSchema(s, opts); + }); + + if (samples.length === 1) { + return samples[0]; + } else if (samples.some(s => s === null)) { + // If one of our samples is null then we should try to surface the first non-null one. + return samples.find(s => s !== null); + } + + // If we still don't have a sample then we should just return whatever the first sample we've + // got is. The sample might not be a _full_ example but it should be enough to act as a sample. + return samples[0]; } const { example, additionalProperties, properties, items } = objectifySchema;