From fbb779c5be6295d7ee7b0ddd57e9a8d946054b55 Mon Sep 17 00:00:00 2001 From: Mathieu Dutour Date: Tue, 5 Nov 2019 12:15:50 -0800 Subject: [PATCH] handle id dereference --- docs/options.md | 1 + lib/index.d.ts | 10 ++++ lib/pointer.js | 32 +++++++++- lib/ref.js | 5 ++ test/specs/internal-with-id/bundled.js | 32 ++++++++++ test/specs/internal-with-id/dereferenced.js | 58 +++++++++++++++++++ .../internal-with-id/internal-with-id.spec.js | 52 +++++++++++++++++ .../internal-with-id/internal-with-id.yaml | 22 +++++++ test/specs/internal-with-id/parsed.js | 32 ++++++++++ 9 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 test/specs/internal-with-id/bundled.js create mode 100644 test/specs/internal-with-id/dereferenced.js create mode 100644 test/specs/internal-with-id/internal-with-id.spec.js create mode 100644 test/specs/internal-with-id/internal-with-id.yaml create mode 100644 test/specs/internal-with-id/parsed.js diff --git a/docs/options.md b/docs/options.md index ee4290a0..4d193a9b 100644 --- a/docs/options.md +++ b/docs/options.md @@ -74,3 +74,4 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference ` |Option(s) |Type |Description |:---------------------|:-------------------|:------------ |`circular`|`boolean` or `"ignore"`|Determines whether [circular `$ref` pointers](README.md#circular-refs) are handled.

If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.

If set to `"ignore"`, then circular references will simply be ignored. No error will be thrown, but the [`$Refs.circular`](refs.md#circular) property will still be set to `true`. +|`id`|`boolean` or `"ignore"`|Determines whether [`$ref` pointers to id](https://json-schema.org/understanding-json-schema/structuring.html#using-id-with-ref) are handled.

If set to `true`, then the pointer will be dereferenced.

If set to `false`, then a `ReferenceError` will be thrown if the schema contains any references to ids.

If set to `"ignore"`, then references to id will simply be ignored. No error will be thrown. diff --git a/lib/index.d.ts b/lib/index.d.ts index e071da95..e9ef0127 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -221,6 +221,16 @@ declare namespace $RefParser { * If set to `"ignore"`, then circular references will simply be ignored. No error will be thrown, but the `$Refs.circular` property will still be set to `true`. */ circular?: boolean | 'ignore' + /** + * Determines whether $ref pointers to id are handled. + * + * If set to `true`, then the pointer will be dereferenced. + * + * If set to `false`, then a `ReferenceError` will be thrown if the schema contains any references to ids. + * + * If set to `"ignore"`, then references to id will simply be ignored. No error will be thrown. (default) + */ + id?: boolean | 'ignore' } } diff --git a/lib/pointer.js b/lib/pointer.js index 9f5baf17..11d86bd4 100644 --- a/lib/pointer.js +++ b/lib/pointer.js @@ -72,7 +72,27 @@ function Pointer ($ref, path, friendlyPath) { * of the resolved value. */ Pointer.prototype.resolve = function (obj, options) { - let tokens = Pointer.parse(this.path); + let tokens = Pointer.parse(this.path, options); + + // it's a reference to an id + if (typeof tokens === "string") { + this.path = tokens; + + // Crawl the object, one token at a time + let def = Object.keys(obj.definitions || {}).find((key) => { + return obj.definitions[key].$id === tokens; + }); + + if (def === undefined) { + throw ono.syntax(`Error resolving $ref pointer "${this.originalPath}". \Id "${tokens}" does not exist.`); + } + + this.value = obj.definitions[def]; + + // Resolve the final value + resolveIf$Ref(this, options); + return this; + } // Crawl the object, one token at a time this.value = obj; @@ -150,9 +170,10 @@ Pointer.prototype.set = function (obj, value, options) { * {@link https://tools.ietf.org/html/rfc6901#section-3} * * @param {string} path + * @param {$RefParserOptions} options * @returns {string[]} */ -Pointer.parse = function (path) { +Pointer.parse = function (path, options) { // Get the JSON pointer from the path's hash let pointer = url.getHash(path).substr(1); @@ -171,7 +192,12 @@ Pointer.parse = function (path) { } if (pointer[0] !== "") { - throw ono.syntax(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`); + if (options && options.dereference.id === true) { + return "#" + pointer[0]; + } + else { + throw ono.syntax(`Invalid $ref pointer "${pointer}". Pointers must begin with "#/"`); + } } return pointer.slice(1); diff --git a/lib/ref.js b/lib/ref.js index 30c69f45..831c8738 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -133,7 +133,12 @@ $Ref.isAllowed$Ref = function (value, options) { // It's an external reference, which is allowed by the options return true; } + else if (value.$ref[0] === "#" && value.$ref.length > 1 && options && typeof options.dereference.id !== "undefined" && options.dereference.id !== "ignore") { + // It's a reference to an id, which is allowed by the options + return true; + } } + return false; }; /** diff --git a/test/specs/internal-with-id/bundled.js b/test/specs/internal-with-id/bundled.js new file mode 100644 index 00000000..27a941e3 --- /dev/null +++ b/test/specs/internal-with-id/bundled.js @@ -0,0 +1,32 @@ +"use strict"; + +module.exports = { + definitions: { + address: { + required: ["streetAddress", "city", "state"], + $id: "#address", + type: "object", + properties: { + streetAddress: { + type: "string" + }, + city: { + type: "string" + }, + state: { + type: "string" + } + } + } + }, + type: "object", + properties: { + billingAddress: { + $ref: "#address" + }, + shippingAddress: { + $ref: "#address" + } + }, + title: "Customer" +}; diff --git a/test/specs/internal-with-id/dereferenced.js b/test/specs/internal-with-id/dereferenced.js new file mode 100644 index 00000000..96e724f0 --- /dev/null +++ b/test/specs/internal-with-id/dereferenced.js @@ -0,0 +1,58 @@ +"use strict"; + +module.exports = { + definitions: { + address: { + required: ["streetAddress", "city", "state"], + $id: "#address", + type: "object", + properties: { + streetAddress: { + type: "string" + }, + city: { + type: "string" + }, + state: { + type: "string" + } + } + } + }, + type: "object", + properties: { + billingAddress: { + $id: "#address", + required: ["streetAddress", "city", "state"], + type: "object", + properties: { + streetAddress: { + type: "string" + }, + city: { + type: "string" + }, + state: { + type: "string" + } + } + }, + shippingAddress: { + $id: "#address", + required: ["streetAddress", "city", "state"], + type: "object", + properties: { + streetAddress: { + type: "string" + }, + city: { + type: "string" + }, + state: { + type: "string" + } + } + } + }, + title: "Customer" +}; diff --git a/test/specs/internal-with-id/internal-with-id.spec.js b/test/specs/internal-with-id/internal-with-id.spec.js new file mode 100644 index 00000000..39c2461e --- /dev/null +++ b/test/specs/internal-with-id/internal-with-id.spec.js @@ -0,0 +1,52 @@ +"use strict"; + +const { expect } = require("chai"); +const $RefParser = require("../../.."); +const helper = require("../../utils/helper"); +const path = require("../../utils/path"); +const parsedSchema = require("./parsed"); +const dereferencedSchema = require("./dereferenced"); +const bundledSchema = require("./bundled"); + +describe("Schema with internal $refs with id", () => { + it("should parse successfully", async () => { + let parser = new $RefParser(); + const schema = await parser.parse( + path.rel("specs/internal-with-id/internal-with-id.yaml") + ); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(parsedSchema); + expect(parser.$refs.paths()).to.deep.equal([ + path.abs("specs/internal-with-id/internal-with-id.yaml") + ]); + }); + + it( + "should resolve successfully", + helper.testResolve( + path.rel("specs/internal-with-id/internal-with-id.yaml"), + path.abs("specs/internal-with-id/internal-with-id.yaml"), + parsedSchema + ) + ); + + it("should dereference successfully", async () => { + let parser = new $RefParser(); + const schema = await parser.dereference( + path.rel("specs/internal-with-id/internal-with-id.yaml"), { dereference: { id: true }} + ); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema); + // The "circular" flag should NOT be set + expect(parser.$refs.circular).to.equal(false); + }); + + it("should bundle successfully", async () => { + let parser = new $RefParser(); + const schema = await parser.bundle( + path.rel("specs/internal-with-id/internal-with-id.yaml") + ); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(bundledSchema); + }); +}); diff --git a/test/specs/internal-with-id/internal-with-id.yaml b/test/specs/internal-with-id/internal-with-id.yaml new file mode 100644 index 00000000..9454b989 --- /dev/null +++ b/test/specs/internal-with-id/internal-with-id.yaml @@ -0,0 +1,22 @@ +title: Customer +type: object +definitions: + address: + $id: "#address" + type: object + properties: + streetAddress: + type: string + city: + type: string + state: + type: string + required: + - streetAddress + - city + - state +properties: + billingAddress: + $ref: "#address" + shippingAddress: + $ref: "#address" diff --git a/test/specs/internal-with-id/parsed.js b/test/specs/internal-with-id/parsed.js new file mode 100644 index 00000000..27a941e3 --- /dev/null +++ b/test/specs/internal-with-id/parsed.js @@ -0,0 +1,32 @@ +"use strict"; + +module.exports = { + definitions: { + address: { + required: ["streetAddress", "city", "state"], + $id: "#address", + type: "object", + properties: { + streetAddress: { + type: "string" + }, + city: { + type: "string" + }, + state: { + type: "string" + } + } + } + }, + type: "object", + properties: { + billingAddress: { + $ref: "#address" + }, + shippingAddress: { + $ref: "#address" + } + }, + title: "Customer" +};