diff --git a/packages/downgrader/README.md b/packages/downgrader/README.md index 8da72e9..330c9fc 100644 --- a/packages/downgrader/README.md +++ b/packages/downgrader/README.md @@ -116,7 +116,7 @@ Schema Objects: | `true` / `false` boolean schemas | `{}` / `{ not: {} }` | | `$ref` with sibling keywords | siblings kept, `$ref` moved into `allOf` | | `type: ["T", "null"]` | `type: "T"` plus `nullable: true` | -| `type` with several non-null entries | `anyOf` of single-type schemas, each `nullable` when `null` was listed | +| `type` with several non-null entries | `anyOf` of single-type schemas, each `nullable` when `null` was listed. A sibling `items` moves into the `array` variant | | `type: "null"` | `enum: [null]`, since 3.0 ignores `nullable` without a `type`. A sibling `enum` or `const` is intersected with the null type: an `enum` containing `null` collapses to `[null]`, and one excluding it yields `not: {}`, since the source accepted no value | | `const` | single-value `enum` | | numeric `exclusiveMinimum` / `exclusiveMaximum` | `minimum` / `maximum` plus the boolean flag; a tighter existing bound wins | diff --git a/packages/downgrader/src/v3.1-to-v3.0.test.ts b/packages/downgrader/src/v3.1-to-v3.0.test.ts index 4c2018c..903c7fd 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.test.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.test.ts @@ -898,13 +898,20 @@ describe('downgradeSchemaV31ToV30', () => { { anyOf: [{ items: {}, type: 'array' }, { type: 'string' }] }, ], [ - 'copies existing items into the synthesized array variant', + 'moves existing items into the synthesized array variant', { items: { type: 'integer' }, type: ['array', 'string', 'null'] }, { anyOf: [ { items: { type: 'integer' }, nullable: true, type: 'array' }, { nullable: true, type: 'string' }, ], + }, + ], + [ + 'keeps items in place when the type union has no array variant', + { items: { type: 'integer' }, type: ['object', 'string'] }, + { + anyOf: [{ type: 'object' }, { type: 'string' }], items: { type: 'integer' }, }, ], @@ -936,9 +943,10 @@ describe('downgradeSchemaV31ToV30', () => { { allOf: 'junk', anyOf: [{ type: 'string' }], - type: ['integer', 'string'], + items: { type: 'integer' }, + type: ['array', 'string'], }, - { allOf: 'junk', anyOf: [{ type: 'string' }] }, + { allOf: 'junk', anyOf: [{ type: 'string' }], items: { type: 'integer' } }, ], [ 'deduplicates type array entries', @@ -1369,6 +1377,16 @@ describe('downgradeSchemaV31ToV30', () => { expect(() => downgradeSchemaV31ToV30(deep)).not.toThrow() }) + it('keeps nested multi-type arrays linear instead of doubling per level', () => { + let input: OpenAPIV3_1.SchemaObject = { type: 'string' } + let expected: unknown = { type: 'string' } + for (let index = 0; index < 10; index += 1) { + input = { items: input, type: ['array', 'object'] } + expected = { anyOf: [{ items: expected, type: 'array' }, { type: 'object' }] } + } + expect(convertSchema(input)).toEqual(expected) + }) + it('converts a dereferenced cyclic schema, pointing the cycle at the converted ancestor', () => { const properties: Record = {} const node: Record = { @@ -1384,5 +1402,15 @@ describe('downgradeSchemaV31ToV30', () => { expect(dig(result, 'properties', 'children', 'items')).toBe(result) expect(node.type).toEqual(['object', 'null']) }) + + it('points the array variant of a cyclic multi-type schema at the converted schema', () => { + const node: Record = { type: ['array', 'object'] } + node.items = node + const result = convertSchema(node) as Record + expect(result).not.toHaveProperty('items') + expect(dig(result, 'anyOf', '0', 'items')).toBe(result) + expect(dig(result, 'anyOf', '1')).toEqual({ type: 'object' }) + expect(node.items).toBe(node) + }) }) }) diff --git a/packages/downgrader/src/v3.1-to-v3.0.ts b/packages/downgrader/src/v3.1-to-v3.0.ts index 7a27a19..e8ef1a4 100644 --- a/packages/downgrader/src/v3.1-to-v3.0.ts +++ b/packages/downgrader/src/v3.1-to-v3.0.ts @@ -58,10 +58,14 @@ function applyTypes(types: string[], schema: Record, out: Recor } return } + if (out.anyOf !== undefined && out.allOf !== undefined && !Array.isArray(out.allOf)) { + return + } const variants = rest.map((item) => { const variant: Record = { type: item } if (item === 'array') { - variant.items = out.items === undefined ? {} : deepClone(out.items) + variant.items = out.items === undefined ? {} : out.items + delete out.items } if (nullable) { variant.nullable = true @@ -71,7 +75,7 @@ function applyTypes(types: string[], schema: Record, out: Recor if (out.anyOf === undefined) { out.anyOf = variants } - else if (out.allOf === undefined || Array.isArray(out.allOf)) { + else { out.allOf = [...(Array.isArray(out.allOf) ? out.allOf : []), { anyOf: variants }] } }