diff --git a/src/parser.test.ts b/src/parser.test.ts index 27a6624..9934506 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -7,6 +7,7 @@ import { ReturnValue, Service, StringLiteral, + Type, validate, Violation, } from 'basketry'; @@ -6255,6 +6256,304 @@ describe('parser', () => { }); }); + describe('intersections', () => { + it('creates types from an allOf with $refs', async () => { + // ARRANGE + const oas = { + openapi: '3.0.1', + info: { title: 'Test', version: '1.0.0', description: 'test' }, + components: { + schemas: { + intersection: { + allOf: [ + { $ref: '#/components/schemas/typeA' }, + { $ref: '#/components/schemas/typeB' }, + ], + }, + typeA: { + type: 'object', + properties: { + propFromA: { type: 'string', example: 'exampleValueA' }, + }, + }, + typeB: { + type: 'object', + properties: { + propFromB: { type: 'number', example: 42 }, + }, + }, + }, + }, + }; + + // ACT + const { service } = await parser(JSON.stringify(oas), absoluteSourcePath); + + // ASSERT + expectService(service).toEqual( + partial({ + types: exact([ + partial({ + kind: 'Type', + name: { value: 'intersection' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'propFromA' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'propFromB' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'number' }, + }, + }), + ]), + }), + partial({ + kind: 'Type', + name: { value: 'typeA' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'propFromA' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + ]), + }), + partial({ + kind: 'Type', + name: { value: 'typeB' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'propFromB' }, + }), + ]), + }), + ]), + }), + ); + }); + + it('creates type from an allOf without $refs', async () => { + // ARRANGE + const oas = { + openapi: '3.0.1', + info: { title: 'Test', version: '1.0.0', description: 'test' }, + components: { + schemas: { + intersection: { + allOf: [ + { + type: 'object', + properties: { + propFromA: { type: 'string', example: 'exampleValueA' }, + }, + }, + { + type: 'object', + properties: { + propFromB: { type: 'number', example: 42 }, + }, + }, + ], + }, + }, + }, + }; + + // ACT + const { service } = await parser(JSON.stringify(oas), absoluteSourcePath); + + // ASSERT + expectService(service).toEqual( + partial({ + types: exact([ + partial({ + kind: 'Type', + name: { value: 'intersection' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'propFromA' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'propFromB' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'number' }, + }, + }), + ]), + }), + ]), + }), + ); + }); + + it('creates type from an allOf with duplicate properties', async () => { + // ARRANGE + const oas = { + openapi: '3.0.1', + info: { title: 'Test', version: '1.0.0', description: 'test' }, + components: { + schemas: { + intersection: { + allOf: [ + { + type: 'object', + properties: { + zprop1: { type: 'string' }, + }, + }, + { + type: 'object', + properties: { + zprop1: { type: 'string' }, + aprop2: { type: 'string' }, + }, + }, + ], + }, + }, + }, + }; + + // ACT + const { service } = await parser(JSON.stringify(oas), absoluteSourcePath); + + // ASSERT + expectService(service).toEqual( + partial({ + types: exact([ + partial({ + kind: 'Type', + name: { value: 'intersection' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'zprop1' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'aprop2' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + ]), + }), + ]), + }), + ); + }); + + it('creates a type from an allOf keeping the last property by name', async () => { + // ARRANGE + const oas = { + openapi: '3.0.1', + info: { title: 'Test', version: '1.0.0', description: 'test' }, + components: { + schemas: { + intersection: { + allOf: [ + { + type: 'object', + properties: { + zprop1: { type: 'string' }, + aprop2: { type: 'string' }, + bprop3: { type: 'string' }, + }, + }, + { + type: 'object', + properties: { + cprop4: { type: 'string' }, + }, + }, + { + type: 'object', + properties: { + zprop1: { type: 'string' }, + aprop2: { type: 'number' }, + }, + }, + ], + }, + }, + }, + }; + + // ACT + const { service } = await parser(JSON.stringify(oas), absoluteSourcePath); + + // ASSERT + expectService(service).toEqual( + partial({ + types: exact([ + partial({ + kind: 'Type', + name: { value: 'intersection' }, + properties: exact([ + partial({ + kind: 'Property', + name: { value: 'bprop3' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'cprop4' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'zprop1' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'string' }, + }, + }), + partial({ + kind: 'Property', + name: { value: 'aprop2' }, + value: { + kind: 'PrimitiveValue', + typeName: { value: 'number' }, + }, + }), + ]), + }), + ]), + }), + ); + }); + }); + describe('unions', () => { it('correctly parses a oneOf without $refs in a body parameter', async () => { const oas = { diff --git a/src/parser.ts b/src/parser.ts index 24aa204..5e7da51 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1793,14 +1793,29 @@ export class OAS3Parser { parentName?: string, ): Property[] { if (allOf) { - return allOf.flatMap((subDef) => { - const resolved = this.resolve(subDef, OAS3.ObjectSchemaNode); - if (!resolved) return []; + const intersectedProperties = allOf + .flatMap((subDef) => { + const resolved = this.resolve(subDef, OAS3.ObjectSchemaNode); + if (!resolved) return []; + + const p = resolved.properties; + const r = safeConcat(resolved.required, required); + return this.parseProperties(p, r, resolved.allOf, parentName); + }) + .reverse(); + + const seenNames: Set = new Set(); + const uniqueProperties: Property[] = []; + for (const property of intersectedProperties) { + if (!seenNames.has(property.name.value)) { + seenNames.add(property.name.value); + uniqueProperties.push(property); + } + } - const p = resolved.properties; - const r = safeConcat(resolved.required, required); - return this.parseProperties(p, r, resolved.allOf, parentName); - }); + const result = uniqueProperties.reverse(); + + return result; } else { const requiredSet = new Set(required?.map((r) => r.value) || []); const props: Property[] = [];