From a7ef007f1c149a7241df97d8a8abb64dc68b321d Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 14:39:26 -0300 Subject: [PATCH 01/12] convert nullable one ofs --- src/converter.ts | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 6d22bff..0c45e7e 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -144,6 +144,7 @@ export class Converter { this.convertJsonSchemaContentMediaType(); this.convertConstToEnum(); this.convertNullableTypeArray(); + this.convertNullableOneOf(); this.removeWebhooksObject(); this.removeUnsupportedSchemaKeywords(); if (this.convertSchemaComments) { @@ -244,6 +245,33 @@ export class Converter { visitSchemaObjects(this.openapi30, schemaVisitor); } + /** + * Convert oneOf with a single null type to + * `nullable: true` and remove the null variant from oneOf. + */ + convertNullableOneOf() { + const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => { + if (schema.hasOwnProperty('oneOf')) { + const oneOf = schema['oneOf']; + const nonTypeNull = oneOf.filter((variant: object) => { + const keys = Object.keys(variant); + return !(keys.length === 1 && keys.includes('type') && variant['type'] === 'null'); + }); + + if (oneOf.length > nonTypeNull.length) { + console.log('had an oneOf!', oneOf); + + const allOf = [{ nullable: true }, { oneOf: nonTypeNull }]; + console.log('allOf', allOf); + delete schema['oneOf']; + schema['allOf'] = allOf; + } + } + return this.walkNestedSchemaObjects(schema, schemaVisitor); + }; + visitSchemaObjects(this.openapi30, schemaVisitor); + } + removeWebhooksObject() { if (Object.hasOwnProperty.call(this.openapi30, 'webhooks')) { this.log(`Deleted webhooks object`); @@ -251,7 +279,14 @@ export class Converter { } } removeUnsupportedSchemaKeywords() { - const keywordsToRemove = ['$id', '$schema', 'unevaluatedProperties', 'contentMediaType', 'patternProperties', 'propertyNames']; + const keywordsToRemove = [ + '$id', + '$schema', + 'unevaluatedProperties', + 'contentMediaType', + 'patternProperties', + 'propertyNames', + ]; const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => { keywordsToRemove.forEach((key) => { if (schema.hasOwnProperty(key)) { @@ -371,7 +406,7 @@ export class Converter { return JSON.stringify(x, null, 2); } /** HTTP methods */ - static readonly HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace' ]; + static readonly HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace']; /** * OpenAPI 3.1 defines a new `openIdConnect` security scheme. * Down-convert the scheme to `oauth2` / authorization code flow. @@ -386,7 +421,7 @@ export class Converter { for (const path in paths) { // filter out path.{$ref, summary, description, parameters, servers} and x-* specification extensions const methods = Object.keys(paths[path]).filter((op) => Converter.HTTP_METHODS.includes(op)); - methods.forEach(method => { + methods.forEach((method) => { const operation = paths[path][method]; const sec = (operation?.security || []) as object[]; sec.forEach((s) => { From 9a81cdf43f37612602522005005750fcf074a935 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 15:12:45 -0300 Subject: [PATCH 02/12] assigning types --- src/converter.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 0c45e7e..1302876 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -12,6 +12,7 @@ import { JsonNode, RefObject, SchemaObject, + isRef, } from './RefVisitor'; /** Lightweight OAS document top-level fields */ @@ -245,6 +246,33 @@ export class Converter { visitSchemaObjects(this.openapi30, schemaVisitor); } + findSchema(ref: string): SchemaObject { + const schemaName = ref.split('/').pop(); + const components = this.openapi30?.components; + const schemas = components && components['schemas']; + if (schemas) { + return schemas[schemaName]; + } + } + + findSchemaObjectType(node: SchemaObject): string { + if (node.hasOwnProperty('type')) { + return node['type']; + } else if (node.hasOwnProperty('allOf') || node.hasOwnProperty('oneOf') || node.hasOwnProperty('anyOf')) { + const variants = node['allOf'] || node['anyOf'] || node['oneOf']; + const types: [string] = variants.map((variant: SchemaObject) => this.findSchemaObjectType(variant)); + const uniqueTypes = [...new Set(types.filter((type) => type !== undefined))]; + if (uniqueTypes.length === 1) { + return uniqueTypes[0]; + } + } else if (isRef(node)) { + const ref = node['$ref']; + const resolvedSchema = this.findSchema(ref); + const type = this.findSchemaObjectType(resolvedSchema); + return type; + } + } + /** * Convert oneOf with a single null type to * `nullable: true` and remove the null variant from oneOf. @@ -259,12 +287,14 @@ export class Converter { }); if (oneOf.length > nonTypeNull.length) { - console.log('had an oneOf!', oneOf); - const allOf = [{ nullable: true }, { oneOf: nonTypeNull }]; - console.log('allOf', allOf); delete schema['oneOf']; schema['allOf'] = allOf; + + const type = this.findSchemaObjectType(schema); + if (type) { + schema['type'] = type; + } } } return this.walkNestedSchemaObjects(schema, schemaVisitor); From 5d81ab196d327e926df03ad3b2fb0bab61390f1b Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 15:18:15 -0300 Subject: [PATCH 03/12] converting oneOfs --- src/converter.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 1302876..a6af193 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -287,14 +287,12 @@ export class Converter { }); if (oneOf.length > nonTypeNull.length) { - const allOf = [{ nullable: true }, { oneOf: nonTypeNull }]; + const type = this.findSchemaObjectType({ oneOf: nonTypeNull }); + const allOf = [{ nullable: true, type }, { oneOf: nonTypeNull }]; delete schema['oneOf']; schema['allOf'] = allOf; - const type = this.findSchemaObjectType(schema); - if (type) { - schema['type'] = type; - } + console.log(schema); } } return this.walkNestedSchemaObjects(schema, schemaVisitor); From 273b2b50804dce0f1903cbb1d27ccd00ae8c83a5 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 15:41:51 -0300 Subject: [PATCH 04/12] inline array type --- src/converter.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index a6af193..e7fa657 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -288,11 +288,16 @@ export class Converter { if (oneOf.length > nonTypeNull.length) { const type = this.findSchemaObjectType({ oneOf: nonTypeNull }); - const allOf = [{ nullable: true, type }, { oneOf: nonTypeNull }]; delete schema['oneOf']; - schema['allOf'] = allOf; - - console.log(schema); + if (type === 'array' && nonTypeNull.length === 1) { + const arraySchema = isRef(nonTypeNull[0]) ? this.findSchema(nonTypeNull[0]['$ref']) : nonTypeNull[0]; + for (const key in Object.keys(arraySchema)) { + schema[key] = arraySchema[key]; + } + } else { + const allOf = [{ nullable: true, type }, { oneOf: nonTypeNull }]; + schema['allOf'] = allOf; + } } } return this.walkNestedSchemaObjects(schema, schemaVisitor); From 78433019cf7c78d6b9f93d99436969154cf12ae1 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 15:43:21 -0300 Subject: [PATCH 05/12] converter --- src/converter.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/converter.ts b/src/converter.ts index e7fa657..7a7b322 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -289,12 +289,16 @@ export class Converter { if (oneOf.length > nonTypeNull.length) { const type = this.findSchemaObjectType({ oneOf: nonTypeNull }); delete schema['oneOf']; + // Nodes with type 'array' must have a sibling 'items' property. + // Thus, we'll inline the array type, if possible. if (type === 'array' && nonTypeNull.length === 1) { const arraySchema = isRef(nonTypeNull[0]) ? this.findSchema(nonTypeNull[0]['$ref']) : nonTypeNull[0]; for (const key in Object.keys(arraySchema)) { schema[key] = arraySchema[key]; } - } else { + } + // Other node types work well with this approach. + else { const allOf = [{ nullable: true, type }, { oneOf: nonTypeNull }]; schema['allOf'] = allOf; } From 4d051082dfee47c0a89b9daad968ddf724bca587 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 15:50:36 -0300 Subject: [PATCH 06/12] comments changes --- src/converter.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 7a7b322..c124cea 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -246,6 +246,12 @@ export class Converter { visitSchemaObjects(this.openapi30, schemaVisitor); } + /** + * Finds the schema object from the components schemas. + * + * @param ref The $ref string value. + * @returns The schema object from the document. + */ findSchema(ref: string): SchemaObject { const schemaName = ref.split('/').pop(); const components = this.openapi30?.components; @@ -255,6 +261,12 @@ export class Converter { } } + /** + * Finds the type of an SchemaObject, walking trough the references. + * + * @param node The node that we want to find the type of. + * @returns The deduced type for this node. + */ findSchemaObjectType(node: SchemaObject): string { if (node.hasOwnProperty('type')) { return node['type']; @@ -274,8 +286,12 @@ export class Converter { } /** - * Convert oneOf with a single null type to - * `nullable: true` and remove the null variant from oneOf. + * OpenAPI 3.1 has a common pattern where an `{ oneOf: [{ type: null }, { .. }]}` + * Is used to represent a nullable type. + * + * Up to this point the conversion would result in a `{ oneOf: [{ nullable: true }, { .. }]}` node. + * Since `nullable: true` must have a sibling `type` property, + * this function adds the type to the `nullable: true` field. */ convertNullableOneOf() { const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => { From 6efeafcf78b7f4a7a96049a9e6c77c5c013e03a9 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:09:38 -0300 Subject: [PATCH 07/12] converts arrays --- src/converter.ts | 3 +- test/converter.spec.ts | 146 +++++++++++++++++++++++++++-------------- 2 files changed, 98 insertions(+), 51 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index c124cea..426a64c 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -309,9 +309,10 @@ export class Converter { // Thus, we'll inline the array type, if possible. if (type === 'array' && nonTypeNull.length === 1) { const arraySchema = isRef(nonTypeNull[0]) ? this.findSchema(nonTypeNull[0]['$ref']) : nonTypeNull[0]; - for (const key in Object.keys(arraySchema)) { + for (const key of Object.keys(arraySchema)) { schema[key] = arraySchema[key]; } + schema['nullable'] = true; } // Other node types work well with this approach. else { diff --git a/test/converter.spec.ts b/test/converter.spec.ts index 38dbd10..375d20c 100644 --- a/test/converter.spec.ts +++ b/test/converter.spec.ts @@ -355,7 +355,6 @@ describe('resolver test suite', () => { done(); }); - test('Remove patternProperties keywords', (done) => { const input = { openapi: '3.1.0', @@ -364,12 +363,12 @@ describe('resolver test suite', () => { a: { type: 'object', properties: { - s: { - type: 'string', - }, + s: { + type: 'string', + }, }, patternProperties: { - "^[a-z{2}-[A-Z]{2,3}]$": { + '^[a-z{2}-[A-Z]{2,3}]$': { type: 'object', unevaluatedProperties: false, properties: { @@ -410,13 +409,13 @@ describe('resolver test suite', () => { components: { schemas: { a: { - type: "object", + type: 'object', propertyNames: { - pattern: "^[A-Za-z_][A-Za-z0-9_]*$", + pattern: '^[A-Za-z_][A-Za-z0-9_]*$', }, additionalProperties: { - type: "string", - } + type: 'string', + }, }, }, }, @@ -426,10 +425,10 @@ describe('resolver test suite', () => { components: { schemas: { a: { - type: "object", + type: 'object', additionalProperties: { - type: "string", - } + type: 'string', + }, }, }, }, @@ -452,7 +451,7 @@ describe('resolver test suite', () => { b: { type: 'string', contentMediaType: 'application/pdf', - maxLength: 5000000 + maxLength: 5000000, }, }, }, @@ -468,7 +467,7 @@ describe('resolver test suite', () => { properties: { b: { type: 'string', - maxLength: 5000000 + maxLength: 5000000, }, }, }, @@ -481,35 +480,34 @@ describe('resolver test suite', () => { done(); }); - - test('Remove webhooks object', (done) => { + test('Remove webhooks object', (done) => { const input = { openapi: '3.1.0', - webhooks: { - newThing: { - post: { - requestBody: { - description: 'Information about a new thing in the system', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/newThing' - } - } - } + webhooks: { + newThing: { + post: { + requestBody: { + description: 'Information about a new thing in the system', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/newThing', + }, + }, }, - responses: { - 200: { - description: 'Return a 200 status to indicate that the data was received successfully' - } - } - } - } - } + }, + responses: { + 200: { + description: 'Return a 200 status to indicate that the data was received successfully', + }, + }, + }, + }, + }, }; const expected = { - openapi: '3.0.3' + openapi: '3.0.3', }; const converter = new Converter(input, { verbose: true }); @@ -831,11 +829,11 @@ test('binary encoded data with existing binary format', (done) => { const converter = new Converter(input); let caught = false; try { - converter.convert(); + converter.convert(); } catch (e) { caught = true; } - expect(caught).toBeTruthy() + expect(caught).toBeTruthy(); // TODO how to check that Converter logged a specific note? done(); }); @@ -909,7 +907,7 @@ test('contentMediaType with existing binary format', (done) => { binaryEncodedDataWithExistingBinaryFormat: { type: 'string', contentMediaType: 'application/octet-stream', - format: 'binary' + format: 'binary', }, }, }, @@ -932,7 +930,6 @@ test('contentMediaType with existing binary format', (done) => { done(); }); - test('contentMediaType with no existing format', (done) => { const input = { openapi: '3.1.0', @@ -971,20 +968,69 @@ test('contentMediaType with existing unexpected format', (done) => { binaryEncodedDataWithExistingBinaryFormat: { type: 'string', contentMediaType: 'application/octet-stream', - format: 'byte' + format: 'byte', }, }, }, }; - const converter = new Converter(input); - let caught = false; - try { - converter.convert(); - } catch (e) { - caught = true; - } - expect(caught).toBeTruthy(); + const converter = new Converter(input); + let caught = false; + try { + converter.convert(); + } catch (e) { + caught = true; + } + expect(caught).toBeTruthy(); // TODO how to check that Converter logged to console.warn ? done(); }); + +test('converts nullable oneOf', (done) => { + const input = { + openapi: '3.1.0', + components: { + schemas: { + ArrayItem: { + type: 'object', + properties: { text: { type: 'string' } }, + }, + ArrayType: { + type: 'array', + items: { $ref: '#/components/ArrayItem' }, + }, + NullableOneOfArray: { + oneOf: [{ type: 'null' }, { $ref: '#/components/ArrayType' }], + }, + }, + }, + }; + + const expected = { + openapi: '3.0.3', + components: { + schemas: { + ArrayItem: { + type: 'object', + properties: { text: { type: 'string' } }, + }, + ArrayType: { + type: 'array', + items: { $ref: '#/components/ArrayItem' }, + }, + NullableOneOfArray: { + nullable: true, + type: 'array', + items: { $ref: '#/components/ArrayItem' }, + }, + }, + }, + }; + + const converter = new Converter(input); + const converted: any = converter.convert(); + + console.log(converted); + expect(converted).toEqual(expected); + done(); +}); From 072689f9e11bd3fa82a8daab7c5f41d6d316eb5c Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:12:40 -0300 Subject: [PATCH 08/12] adds tes for non array types --- test/converter.spec.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/test/converter.spec.ts b/test/converter.spec.ts index 375d20c..60c7400 100644 --- a/test/converter.spec.ts +++ b/test/converter.spec.ts @@ -986,7 +986,7 @@ test('contentMediaType with existing unexpected format', (done) => { done(); }); -test('converts nullable oneOf', (done) => { +test('converts nullable oneOf with an array', (done) => { const input = { openapi: '3.1.0', components: { @@ -1034,3 +1034,42 @@ test('converts nullable oneOf', (done) => { expect(converted).toEqual(expected); done(); }); + +test('converts nullable oneOf with an object type', (done) => { + const input = { + openapi: '3.1.0', + components: { + schemas: { + Object: { + type: 'object', + properties: { text: { type: 'string' } }, + }, + NullableOneOfArray: { + oneOf: [{ type: 'null' }, { $ref: '#/components/Object' }], + }, + }, + }, + }; + + const expected = { + openapi: '3.0.3', + components: { + schemas: { + Object: { + type: 'object', + properties: { text: { type: 'string' } }, + }, + NullableOneOfArray: { + allOf: [{ nullable: true, type: 'object' }, { oneOf: [{ $ref: '#/components/Object' }] }], + }, + }, + }, + }; + + const converter = new Converter(input); + const converted: any = converter.convert(); + + console.log(converted); + expect(converted).toEqual(expected); + done(); +}); From b44b8be17cc53dd0e77d353482a2481be84f6f26 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:23:13 -0300 Subject: [PATCH 09/12] undo formatting changes --- src/converter.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 426a64c..4de6078 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -333,14 +333,7 @@ export class Converter { } } removeUnsupportedSchemaKeywords() { - const keywordsToRemove = [ - '$id', - '$schema', - 'unevaluatedProperties', - 'contentMediaType', - 'patternProperties', - 'propertyNames', - ]; + const keywordsToRemove = ['$id', '$schema', 'unevaluatedProperties', 'contentMediaType', 'patternProperties', 'propertyNames']; const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => { keywordsToRemove.forEach((key) => { if (schema.hasOwnProperty(key)) { @@ -460,7 +453,7 @@ export class Converter { return JSON.stringify(x, null, 2); } /** HTTP methods */ - static readonly HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace']; + static readonly HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace' ]; /** * OpenAPI 3.1 defines a new `openIdConnect` security scheme. * Down-convert the scheme to `oauth2` / authorization code flow. @@ -475,7 +468,7 @@ export class Converter { for (const path in paths) { // filter out path.{$ref, summary, description, parameters, servers} and x-* specification extensions const methods = Object.keys(paths[path]).filter((op) => Converter.HTTP_METHODS.includes(op)); - methods.forEach((method) => { + methods.forEach(method => { const operation = paths[path][method]; const sec = (operation?.security || []) as object[]; sec.forEach((s) => { From c16043e013ecba4a069b1cde7b47ef2c0696e9bb Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:30:23 -0300 Subject: [PATCH 10/12] undo tests formatting changes --- test/converter.spec.ts | 51 ++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/test/converter.spec.ts b/test/converter.spec.ts index 60c7400..0dcb528 100644 --- a/test/converter.spec.ts +++ b/test/converter.spec.ts @@ -355,6 +355,7 @@ describe('resolver test suite', () => { done(); }); + test('Remove patternProperties keywords', (done) => { const input = { openapi: '3.1.0', @@ -368,7 +369,7 @@ describe('resolver test suite', () => { }, }, patternProperties: { - '^[a-z{2}-[A-Z]{2,3}]$': { + "^[a-z{2}-[A-Z]{2,3}]$": { type: 'object', unevaluatedProperties: false, properties: { @@ -409,13 +410,13 @@ describe('resolver test suite', () => { components: { schemas: { a: { - type: 'object', + type: "object", propertyNames: { - pattern: '^[A-Za-z_][A-Za-z0-9_]*$', + pattern: "^[A-Za-z_][A-Za-z0-9_]*$", }, additionalProperties: { - type: 'string', - }, + type: "string", + } }, }, }, @@ -425,10 +426,10 @@ describe('resolver test suite', () => { components: { schemas: { a: { - type: 'object', + type: "object", additionalProperties: { - type: 'string', - }, + type: "string", + } }, }, }, @@ -451,7 +452,7 @@ describe('resolver test suite', () => { b: { type: 'string', contentMediaType: 'application/pdf', - maxLength: 5000000, + maxLength: 5000000 }, }, }, @@ -467,7 +468,7 @@ describe('resolver test suite', () => { properties: { b: { type: 'string', - maxLength: 5000000, + maxLength: 5000000 }, }, }, @@ -480,6 +481,7 @@ describe('resolver test suite', () => { done(); }); + test('Remove webhooks object', (done) => { const input = { openapi: '3.1.0', @@ -491,23 +493,23 @@ describe('resolver test suite', () => { content: { 'application/json': { schema: { - $ref: '#/components/schemas/newThing', - }, - }, - }, + $ref: '#/components/schemas/newThing' + } + } + } }, responses: { 200: { - description: 'Return a 200 status to indicate that the data was received successfully', - }, - }, - }, - }, - }, + description: 'Return a 200 status to indicate that the data was received successfully' + } + } + } + } + } }; const expected = { - openapi: '3.0.3', + openapi: '3.0.3' }; const converter = new Converter(input, { verbose: true }); @@ -833,7 +835,7 @@ test('binary encoded data with existing binary format', (done) => { } catch (e) { caught = true; } - expect(caught).toBeTruthy(); + expect(caught).toBeTruthy() // TODO how to check that Converter logged a specific note? done(); }); @@ -907,7 +909,7 @@ test('contentMediaType with existing binary format', (done) => { binaryEncodedDataWithExistingBinaryFormat: { type: 'string', contentMediaType: 'application/octet-stream', - format: 'binary', + format: 'binary' }, }, }, @@ -930,6 +932,7 @@ test('contentMediaType with existing binary format', (done) => { done(); }); + test('contentMediaType with no existing format', (done) => { const input = { openapi: '3.1.0', @@ -968,7 +971,7 @@ test('contentMediaType with existing unexpected format', (done) => { binaryEncodedDataWithExistingBinaryFormat: { type: 'string', contentMediaType: 'application/octet-stream', - format: 'byte', + format: 'byte' }, }, }, From 104005e46be4489b1377f640d6bf617dfbdae2ba Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:47:57 -0300 Subject: [PATCH 11/12] only insert type if type was found --- src/converter.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/converter.ts b/src/converter.ts index 4de6078..0d8bc6b 100644 --- a/src/converter.ts +++ b/src/converter.ts @@ -253,11 +253,15 @@ export class Converter { * @returns The schema object from the document. */ findSchema(ref: string): SchemaObject { - const schemaName = ref.split('/').pop(); - const components = this.openapi30?.components; - const schemas = components && components['schemas']; - if (schemas) { - return schemas[schemaName]; + const prefix = "#/components/schemas/"; + const schemaName = ref.startsWith(prefix) && ref.slice(prefix.length); + + if (schemaName) { + const components = this.openapi30?.components; + const schemas = components && components['schemas']; + if (schemas) { + return schemas[schemaName]; + } } } @@ -280,8 +284,10 @@ export class Converter { } else if (isRef(node)) { const ref = node['$ref']; const resolvedSchema = this.findSchema(ref); - const type = this.findSchemaObjectType(resolvedSchema); - return type; + if (resolvedSchema) { + const type = this.findSchemaObjectType(resolvedSchema); + return type; + } } } @@ -304,10 +310,10 @@ export class Converter { if (oneOf.length > nonTypeNull.length) { const type = this.findSchemaObjectType({ oneOf: nonTypeNull }); - delete schema['oneOf']; // Nodes with type 'array' must have a sibling 'items' property. // Thus, we'll inline the array type, if possible. if (type === 'array' && nonTypeNull.length === 1) { + delete schema['oneOf']; const arraySchema = isRef(nonTypeNull[0]) ? this.findSchema(nonTypeNull[0]['$ref']) : nonTypeNull[0]; for (const key of Object.keys(arraySchema)) { schema[key] = arraySchema[key]; @@ -315,7 +321,8 @@ export class Converter { schema['nullable'] = true; } // Other node types work well with this approach. - else { + else if (type) { + delete schema['oneOf']; const allOf = [{ nullable: true, type }, { oneOf: nonTypeNull }]; schema['allOf'] = allOf; } From f52ac6b59184fac06ace957a5a4bbf24b32b2097 Mon Sep 17 00:00:00 2001 From: Emilio Wuerges Date: Thu, 13 Feb 2025 16:49:21 -0300 Subject: [PATCH 12/12] fix schema paths --- test/converter.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/converter.spec.ts b/test/converter.spec.ts index 0dcb528..14e0e4a 100644 --- a/test/converter.spec.ts +++ b/test/converter.spec.ts @@ -1000,10 +1000,10 @@ test('converts nullable oneOf with an array', (done) => { }, ArrayType: { type: 'array', - items: { $ref: '#/components/ArrayItem' }, + items: { $ref: '#/components/schemas/ArrayItem' }, }, NullableOneOfArray: { - oneOf: [{ type: 'null' }, { $ref: '#/components/ArrayType' }], + oneOf: [{ type: 'null' }, { $ref: '#/components/schemas/ArrayType' }], }, }, }, @@ -1019,12 +1019,12 @@ test('converts nullable oneOf with an array', (done) => { }, ArrayType: { type: 'array', - items: { $ref: '#/components/ArrayItem' }, + items: { $ref: '#/components/schemas/ArrayItem' }, }, NullableOneOfArray: { nullable: true, type: 'array', - items: { $ref: '#/components/ArrayItem' }, + items: { $ref: '#/components/schemas/ArrayItem' }, }, }, }, @@ -1048,7 +1048,7 @@ test('converts nullable oneOf with an object type', (done) => { properties: { text: { type: 'string' } }, }, NullableOneOfArray: { - oneOf: [{ type: 'null' }, { $ref: '#/components/Object' }], + oneOf: [{ type: 'null' }, { $ref: '#/components/schemas/Object' }], }, }, }, @@ -1063,7 +1063,7 @@ test('converts nullable oneOf with an object type', (done) => { properties: { text: { type: 'string' } }, }, NullableOneOfArray: { - allOf: [{ nullable: true, type: 'object' }, { oneOf: [{ $ref: '#/components/Object' }] }], + allOf: [{ nullable: true, type: 'object' }, { oneOf: [{ $ref: '#/components/schemas/Object' }] }], }, }, },