From 34b6dc297341951511d7b6726034bd67c0580c91 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Mon, 29 Jan 2024 19:33:44 +0900 Subject: [PATCH] fix: edge case where references was incorrect handled for JSON Schema (#1754) --- package-lock.json | 78 ++++++++++++++++--- package.json | 2 +- src/processors/JsonSchemaInputProcessor.ts | 26 ++++--- .../JsonSchemaInputProcessor.spec.ts | 34 +++++++- 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56096e84ee..cce2466977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.0-next.13", "license": "Apache-2.0", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@apidevtools/json-schema-ref-parser": "^11.1.0", "@apidevtools/swagger-parser": "^10.0.3", "@asyncapi/parser": "3.0.0-next-major-spec.8", "@smoya/multi-parser": "^4.0.0", @@ -58,14 +58,21 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", - "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.1.0.tgz", + "integrity": "sha512-g/VW9ZQEFJAOwAyUb8JFf7MLiLy2uEB4rU270rGzDwICxnxMlPy0O11KVePSgS36K1NI29gSlK84n5INGhd4Ag==", "dependencies": { "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", - "js-yaml": "^4.1.0" + "@types/json-schema": "^7.0.13", + "@types/lodash.clonedeep": "^4.5.7", + "js-yaml": "^4.1.0", + "lodash.clonedeep": "^4.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" } }, "node_modules/@apidevtools/openapi-schemas": { @@ -97,6 +104,17 @@ "openapi-types": ">=7" } }, + "node_modules/@apidevtools/swagger-parser/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, "node_modules/@asyncapi/avro-schema-parser": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@asyncapi/avro-schema-parser/-/avro-schema-parser-3.0.4.tgz", @@ -283,6 +301,36 @@ "tiny-merge-patch": "^0.1.2" } }, + "node_modules/@asyncapi/parserV1/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@asyncapi/parserV1/node_modules/@apidevtools/json-schema-ref-parser/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@asyncapi/parserV1/node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@asyncapi/parserV1/node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2495,6 +2543,19 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "20.8.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz", @@ -9067,8 +9128,7 @@ "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "node_modules/lodash.get": { "version": "4.4.2", diff --git a/package.json b/package.json index 581925f145..00d9517880 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "./LICENSE" ], "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@apidevtools/json-schema-ref-parser": "^11.1.0", "@apidevtools/swagger-parser": "^10.0.3", "@asyncapi/parser": "3.0.0-next-major-spec.8", "@smoya/multi-parser": "^4.0.0", diff --git a/src/processors/JsonSchemaInputProcessor.ts b/src/processors/JsonSchemaInputProcessor.ts index b64aae1a66..e5b9071464 100644 --- a/src/processors/JsonSchemaInputProcessor.ts +++ b/src/processors/JsonSchemaInputProcessor.ts @@ -1,5 +1,5 @@ import { AbstractInputProcessor } from './AbstractInputProcessor'; -import $RefParser from '@apidevtools/json-schema-ref-parser'; +import { dereference } from '@apidevtools/json-schema-ref-parser'; import path from 'path'; import { CommonModel, @@ -15,6 +15,7 @@ import { import { Logger } from '../utils'; import { Interpreter } from '../interpreter/Interpreter'; import { convertToMetaModel } from '../helpers'; +import { ParserOptions } from '@apidevtools/json-schema-ref-parser/dist/lib/options'; /** * Class for processing JSON Schema @@ -84,7 +85,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { {}, 'root', true - ) as any; + ); input = await this.dereferenceInputs(input); const parsedSchema = Draft7Schema.toSchema(input); const newCommonModel = JsonSchemaInputProcessor.convertSchemaToCommonModel( @@ -114,7 +115,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { {}, 'root', true - ) as any; + ); input = await this.dereferenceInputs(input); const parsedSchema = Draft4Schema.toSchema(input); const newCommonModel = JsonSchemaInputProcessor.convertSchemaToCommonModel( @@ -144,7 +145,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { {}, 'root', true - ) as any; + ); input = await this.dereferenceInputs(input); const parsedSchema = Draft6Schema.toSchema(input); const newCommonModel = JsonSchemaInputProcessor.convertSchemaToCommonModel( @@ -197,12 +198,19 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { public async dereferenceInputs(input: any): Promise { input = this.handleRootReference(input); Logger.debug('Dereferencing all $ref instances'); - const refParser = new $RefParser(); // eslint-disable-next-line no-undef const localPath = `${process.cwd()}${path.sep}`; - const deRefOption: $RefParser.Options = { + const deRefOption: ParserOptions = { continueOnError: true, - dereference: { circular: true } + dereference: { + circular: true, + excludedPathMatcher: (path: string) => { + return ( + path.includes('/examples/') && + !path.includes('/properties/examples/') + ); + } + } }; Logger.debug( `Trying to dereference all $ref instances from input, using option ${JSON.stringify( @@ -210,7 +218,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { )}.` ); try { - await refParser.dereference(localPath, input, deRefOption); + await dereference(localPath, input, deRefOption); } catch (e: any) { const errorMessage = `Could not dereference $ref in input, is all the references correct? ${e.message}`; Logger.error(errorMessage, e); @@ -252,7 +260,7 @@ export class JsonSchemaInputProcessor extends AbstractInputProcessor { return schema; } - schema = Object.assign({}, schema); + schema = { ...schema }; if (isRoot) { namesStack[String(name)] = 0; (schema as any)[this.MODELGEN_INFFERED_NAME] = name; diff --git a/test/processors/JsonSchemaInputProcessor.spec.ts b/test/processors/JsonSchemaInputProcessor.spec.ts index 73737ae515..cc3e856a4b 100644 --- a/test/processors/JsonSchemaInputProcessor.spec.ts +++ b/test/processors/JsonSchemaInputProcessor.spec.ts @@ -5,7 +5,7 @@ import { AnyModel, CommonModel } from '../../src/models'; jest.mock('../../src/utils/LoggingInterface'); jest.spyOn(JsonSchemaInputProcessor, 'convertSchemaToCommonModel'); let mockedReturnModels = [new CommonModel()]; -const mockedMetaModel = new AnyModel('test', undefined); +const mockedMetaModel = new AnyModel('test', undefined, {}); jest.mock('../../src/helpers/CommonModelToMetaModel', () => { return { convertToMetaModel: jest.fn().mockImplementation(() => { @@ -261,6 +261,38 @@ describe('JsonSchemaInputProcessor', () => { 'Cannot handle input, because it has a root `$ref`, please manually resolve the first reference.' ); }); + test('should not resolve example containing $ref keyword', async () => { + const processor = new JsonSchemaInputProcessor(); + const schema = { + examples: [{ $ref: '#/none/existing/reference' }], + properties: { + $ref: { + type: 'string' + } + } + }; + const dereferencedSchema = await processor.dereferenceInputs(schema); + expect(dereferencedSchema.examples[0]).toEqual({ + $ref: '#/none/existing/reference' + }); + }); + test('should resolve example containing $ref keyword if part of properties', async () => { + const processor = new JsonSchemaInputProcessor(); + const schema = { + definitions: { + reference: { + type: 'string' + } + }, + properties: { + $ref: { $ref: '#/definitions/reference' } + } + }; + const dereferencedSchema = await processor.dereferenceInputs(schema); + expect(dereferencedSchema.properties.$ref).toEqual({ + type: 'string' + }); + }); }); describe('convertSchemaToCommonModel()', () => {