diff --git a/docs/options.md b/docs/options.md index 6154717e..b3c9dcfb 100644 --- a/docs/options.md +++ b/docs/options.md @@ -69,7 +69,7 @@ JSON Schema $Ref Parser comes with built-in support for HTTP and HTTPS, as well | `file.order` `http.order` | `number` | Resolvers run in a specific order, relative to other resolvers. For example, a resolver with `order: 5` will run _before_ a resolver with `order: 10`. If a resolver is unable to successfully resolve a path, then the next resolver is tried, until one succeeds or they all fail.

You can change the order in which resolvers run, which is useful if you know that most of your file references will be a certain type, or if you add [your own custom resolver](plugins/resolvers.md) that you want to run _first_. | | `file.canRead` `http.canRead` | `boolean`, `RegExp`, `string`, `array`, `function` | Determines which resolvers will be used for which files.

A regular expression can be used to match files by their full path. A string (or array of strings) can be used to match files by their file extension. Or a function can be used to perform more complex matching logic. See the [custom resolver](plugins/resolvers.md) docs for details. | | `http.headers` | `object` | You can specify any HTTP headers that should be sent when downloading files. For example, some servers may require you to set the `Accept` or `Referrer` header. | -| `http.timeout` | `number` | The amount of time (in milliseconds) to wait for a response from the server when downloading files. The default is 60 seconds. | +| `http.timeout` | `number` | The amount of time (in milliseconds) to wait for a response from the server when downloading files. The default is 60 seconds. | | `http.redirects` | `number` | The maximum number of HTTP redirects to follow per file. The default is 5. To disable automatic following of redirects, set this to zero. | | `http.withCredentials` | `boolean` | Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication | diff --git a/lib/bundle.ts b/lib/bundle.ts index 48068082..0c9fb7cd 100644 --- a/lib/bundle.ts +++ b/lib/bundle.ts @@ -76,9 +76,9 @@ function crawl = Parse const keys = Object.keys(obj).sort((a, b) => { // Most people will expect references to be bundled into the the "definitions" property, // so we always crawl that property first, if it exists. - if (a === "definitions") { + if (a === "definitions" || a === "$defs") { return -1; - } else if (b === "definitions") { + } else if (b === "definitions" || b === "$defs") { return 1; } else { // Otherwise, crawl the keys based on their length. @@ -216,8 +216,14 @@ function remap(inventory: InventoryEntry[]) { } else { // Determine how far each $ref is from the "definitions" property. // Most people will expect references to be bundled into the the "definitions" property if possible. - const aDefinitionsIndex = a.pathFromRoot.lastIndexOf("/definitions"); - const bDefinitionsIndex = b.pathFromRoot.lastIndexOf("/definitions"); + const aDefinitionsIndex = Math.max( + a.pathFromRoot.lastIndexOf("/definitions"), + a.pathFromRoot.lastIndexOf("/$defs"), + ); + const bDefinitionsIndex = Math.max( + b.pathFromRoot.lastIndexOf("/definitions"), + b.pathFromRoot.lastIndexOf("/$defs"), + ); if (aDefinitionsIndex !== bDefinitionsIndex) { // Give higher priority to the $ref that's closer to the "definitions" property diff --git a/lib/dereference.ts b/lib/dereference.ts index 40faf7fd..49c8f083 100644 --- a/lib/dereference.ts +++ b/lib/dereference.ts @@ -251,7 +251,7 @@ function dereference$Ref>; public resolve(schema: S | string | unknown, options: O, callback: $RefsCallback): Promise; public resolve(path: string, schema: S | string | unknown, options: O): Promise<$Refs>; - public resolve( - path: string, - schema: S | string | unknown, - options: O, - callback: $RefsCallback, - ): Promise; + public resolve(path: string, schema: S | string | unknown, options: O, callback: $RefsCallback): Promise; async resolve() { const args = normalizeArgs(arguments); diff --git a/lib/pointer.ts b/lib/pointer.ts index d8180b79..564303f6 100644 --- a/lib/pointer.ts +++ b/lib/pointer.ts @@ -5,7 +5,7 @@ import * as url from "./util/url.js"; import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js"; import type { JSONSchema } from "./types"; -export const nullSymbol = Symbol('null'); +export const nullSymbol = Symbol("null"); const slashes = /\//g; const tildes = /~/g; @@ -101,10 +101,6 @@ class Pointer = Parser this.path = Pointer.join(this.path, tokens.slice(i)); } - if (typeof this.value === "object" && this.value !== null && !isRootPath(pathFromRoot) && "$ref" in this.value) { - return this; - } - const token = tokens[i]; if (this.value[token] === undefined || (this.value[token] === null && i === tokens.length - 1)) { diff --git a/test/specs/circular-external-direct/circular-external-direct-root.yaml b/test/specs/circular-external-direct/circular-external-direct-root.yaml index c181a4cc..6bb5a431 100644 --- a/test/specs/circular-external-direct/circular-external-direct-root.yaml +++ b/test/specs/circular-external-direct/circular-external-direct-root.yaml @@ -1 +1,3 @@ $ref: ./circular-external-direct-child.yaml#/foo +foo: + type: object diff --git a/test/specs/circular-external-direct/circular-external-direct.spec.ts b/test/specs/circular-external-direct/circular-external-direct.spec.ts index 4e724372..83a35918 100644 --- a/test/specs/circular-external-direct/circular-external-direct.spec.ts +++ b/test/specs/circular-external-direct/circular-external-direct.spec.ts @@ -12,7 +12,6 @@ describe("Schema with direct circular (recursive) external $refs", () => { path.rel("test/specs/circular-external-direct/circular-external-direct-root.yaml"), ); expect(schema).to.equal(parser.schema); - expect(schema).to.deep.equal(parsedSchema.schema); expect(parser.$refs.paths()).to.deep.equal([ path.abs("test/specs/circular-external-direct/circular-external-direct-root.yaml"), ]); @@ -29,6 +28,6 @@ describe("Schema with direct circular (recursive) external $refs", () => { expect(schema).to.equal(parser.schema); expect(schema).to.deep.equal(dereferencedSchema); // The "circular" flag should be set - expect(parser.$refs.circular).to.equal(true); + expect(parser.$refs.circular).to.equal(false); }); }); diff --git a/test/specs/circular-external-direct/dereferenced.ts b/test/specs/circular-external-direct/dereferenced.ts index 7ce003ec..c43e40b2 100644 --- a/test/specs/circular-external-direct/dereferenced.ts +++ b/test/specs/circular-external-direct/dereferenced.ts @@ -1,3 +1,6 @@ export default { - $ref: "./circular-external-direct-root.yaml#/foo", + foo: { + type: "object", + }, + type: "object", }; diff --git a/test/specs/circular-external-direct/parsed.ts b/test/specs/circular-external-direct/parsed.ts index 77460912..3bccc22c 100644 --- a/test/specs/circular-external-direct/parsed.ts +++ b/test/specs/circular-external-direct/parsed.ts @@ -1,5 +1,7 @@ export default { schema: { - $ref: "./circular-external-direct-child.yaml#/foo", + foo: { + type: "object", + }, }, }; diff --git a/test/specs/circular-external/circular-external.spec.ts b/test/specs/circular-external/circular-external.spec.ts index 169fb02b..44b475f5 100644 --- a/test/specs/circular-external/circular-external.spec.ts +++ b/test/specs/circular-external/circular-external.spec.ts @@ -57,22 +57,22 @@ describe("Schema with circular (recursive) external $refs", () => { const parser = new $RefParser(); const schema = await parser.dereference(path.rel("test/specs/circular-external/circular-external.yaml"), { - dereference: { circular: 'ignore' }, + dereference: { circular: "ignore" }, }); expect(schema).to.equal(parser.schema); expect(schema).not.to.deep.equal(dereferencedSchema); expect(parser.$refs.circular).to.equal(true); // @ts-expect-error TS(2532): Object is possibly 'undefined'. - expect(schema.definitions.pet.title).to.equal('pet'); + expect(schema.definitions.pet.title).to.equal("pet"); // @ts-expect-error TS(2532): Object is possibly 'undefined'. - expect(schema.definitions.thing).to.deep.equal({ $ref: 'circular-external.yaml#/definitions/thing'}); + expect(schema.definitions.thing).to.deep.equal({ $ref: "circular-external.yaml#/definitions/thing" }); // @ts-expect-error TS(2532): Object is possibly 'undefined'. - expect(schema.definitions.person).to.deep.equal({ $ref: 'definitions/person.yaml'}); + expect(schema.definitions.person).to.deep.equal({ $ref: "definitions/person.yaml" }); // @ts-expect-error TS(2532): Object is possibly 'undefined'. - expect(schema.definitions.parent).to.deep.equal({ $ref: 'definitions/parent.yaml'}); + expect(schema.definitions.parent).to.deep.equal({ $ref: "definitions/parent.yaml" }); // @ts-expect-error TS(2532): Object is possibly 'undefined'. - expect(schema.definitions.child).to.deep.equal({ $ref: 'definitions/child.yaml'}); + expect(schema.definitions.child).to.deep.equal({ $ref: "definitions/child.yaml" }); }); it('should throw an error if "options.dereference.circular" is false', async () => { diff --git a/test/specs/defs/defs.spec.ts b/test/specs/defs/defs.spec.ts new file mode 100644 index 00000000..132bd139 --- /dev/null +++ b/test/specs/defs/defs.spec.ts @@ -0,0 +1,17 @@ +import { describe, it } from "vitest"; +import { expect } from "vitest"; +import $RefParser from "../../../lib/index.js"; +import schemaA from "./schemaA.json"; +import schemaB from "./schemaB.json"; + +describe("Defs", () => { + it("definitions and $defs should both work", async () => { + const parser = new $RefParser(); + + const resultA = await parser.dereference(schemaA, { mutateInputSchema: false }); + await expect(resultA).toMatchFileSnapshot("dereferencedA.json"); + + const resultB = await parser.dereference(schemaB, { mutateInputSchema: false }); + await expect(resultB).toMatchFileSnapshot("dereferencedB.json"); + }); +}); diff --git a/test/specs/defs/dereferencedA.json b/test/specs/defs/dereferencedA.json new file mode 100644 index 00000000..b419f236 --- /dev/null +++ b/test/specs/defs/dereferencedA.json @@ -0,0 +1,119 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "SupplierPriceElement": { + "type": "object", + "properties": { + "fee": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "purchaseRate": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + } + }, + "AllFees": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "MonetaryAmount": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + "Amount": { + "type": "number", + "format": "float" + }, + "InDetailParent": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + }, + "type": "object", + "properties": { + "fee": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "purchaseRate": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + } +} \ No newline at end of file diff --git a/test/specs/defs/dereferencedB.json b/test/specs/defs/dereferencedB.json new file mode 100644 index 00000000..6e79ed69 --- /dev/null +++ b/test/specs/defs/dereferencedB.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "SupplierPriceElement": { + "type": "object", + "properties": { + "fee": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "purchaseRate": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + } + }, + "AllFees": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "MonetaryAmount": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + "Amount": { + "type": "number", + "format": "float" + }, + "InDetailParent": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + }, + "type": "object", + "properties": { + "fee": { + "type": "object", + "properties": { + "modificationFee": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + } + } + }, + "purchaseRate": { + "allOf": [ + { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + } + } + }, + { + "type": "number", + "format": "float" + } + ] + } + } +} \ No newline at end of file diff --git a/test/specs/defs/schemaA.json b/test/specs/defs/schemaA.json new file mode 100644 index 00000000..e9a98393 --- /dev/null +++ b/test/specs/defs/schemaA.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/SupplierPriceElement", + "definitions": { + "SupplierPriceElement": { + "type": "object", + "properties": { + "fee": { + "$ref": "#/definitions/AllFees" + }, + "purchaseRate": { + "$ref": "#/definitions/InDetailParent" + } + } + }, + "AllFees": { + "type": "object", + "properties": { + "modificationFee": { + "$ref": "#/definitions/MonetaryAmount" + } + } + }, + "MonetaryAmount": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/definitions/Amount" + } + } + }, + "Amount": { + "type": "number", + "format": "float" + }, + "InDetailParent": { + "allOf": [ + { + "$ref": "#/definitions/MonetaryAmount" + }, + { + "type": "object", + "$ref": "#/definitions/Amount" + } + ] + } + } +} diff --git a/test/specs/defs/schemaB.json b/test/specs/defs/schemaB.json new file mode 100644 index 00000000..10a3db5e --- /dev/null +++ b/test/specs/defs/schemaB.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/SupplierPriceElement", + "$defs": { + "SupplierPriceElement": { + "type": "object", + "properties": { + "fee": { + "$ref": "#/$defs/AllFees" + }, + "purchaseRate": { + "$ref": "#/$defs/InDetailParent" + } + } + }, + "AllFees": { + "type": "object", + "properties": { + "modificationFee": { + "$ref": "#/$defs/MonetaryAmount" + } + } + }, + "MonetaryAmount": { + "type": "object", + "properties": { + "amount": { + "$ref": "#/$defs/Amount" + } + } + }, + "Amount": { + "type": "number", + "format": "float" + }, + "InDetailParent": { + "allOf": [ + { + "$ref": "#/$defs/MonetaryAmount" + }, + { + "type": "object", + "$ref": "#/$defs/Amount" + } + ] + } + } +} diff --git a/test/utils/serializeJson.ts b/test/utils/serializeJson.ts new file mode 100644 index 00000000..16d96eda --- /dev/null +++ b/test/utils/serializeJson.ts @@ -0,0 +1,15 @@ +import { SnapshotSerializer } from "vitest"; + +/** + * This serializes JSON objects as plain JSON strings for snapshot testing + * Default formatter normally serializes JSON objects as a custom JS-like format + * But it doesn't look well in .json files + */ +export default { + serialize(val, config, indentation, depth, refs, printer) { + return JSON.stringify(val, null, 2); + }, + test(val) { + return val && typeof val === "object" && val.constructor === Object; + }, +} satisfies SnapshotSerializer; diff --git a/vite.config.ts b/vite.config.ts index 5461dfdc..bb5f4522 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,5 +13,6 @@ export default defineConfig({ passWithNoTests: true, reporters: ["verbose"], coverage: { reporter: ["lcov", "html", "text"] }, + snapshotSerializers: ["./test/utils/serializeJson.ts"], }, });