diff --git a/README.md b/README.md index 5d5c5e0e..0b46b7ab 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ JSON Schema $Ref Parser is a full [JSON Reference](https://tools.ietf.org/html/d - Can [dereference](https://apidevtools.org/json-schema-ref-parser/docs/ref-parser.html#dereferencepath-options-callback) your schema, producing a plain-old JavaScript object that's easy to work with - Supports [circular references](https://apidevtools.org/json-schema-ref-parser/docs/#circular-refs), nested references, back-references, and cross-references between files - Maintains object reference equality — `$ref` pointers to the same value always resolve to the same object instance +- Can selectively `resolve` (and therefore [dereference](https://apidevtools.org/json-schema-ref-parser/docs/ref-parser.html#dereferencepath-options-callback)) only some references `$ref` pointers based upon a custom javascript function. - [Tested](https://travis-ci.com/APIDevTools/json-schema-ref-parser) in Node and all major web browsers on Windows, Mac, and Linux diff --git a/docs/options.md b/docs/options.md index ee4290a0..13603346 100644 --- a/docs/options.md +++ b/docs/options.md @@ -28,7 +28,15 @@ $RefParser.dereference("my-schema.yaml", { } }, dereference: { - circular: false // Don't allow circular $refs + circular: false // Don't allow circular $refs + + // custom function that decides if a particular $ref is allowed to be resolved. + isRefResolved: function(value) { + + // resolve the reference if it doesn't match /NZCodeSets or /ISO + return value.$ref.match(/\/NZCodeSets/g) === null && + value.$ref.match(/\/ISO/g) === null + } } }); ``` @@ -74,3 +82,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`. +|`isRefResolved`|`function (value)` | Custom javascript function that determines if particular `$ref` pointers will be resolved or not. e.g. based upon the `$ref` value matching a `regexp`.

Function takes a single `value` parameter and returns `true` if the `$ref` is to be resolved; `false` if the `$ref` is not to be resolved| \ No newline at end of file diff --git a/lib/ref.js b/lib/ref.js index 30c69f45..6d15a6ac 100644 --- a/lib/ref.js +++ b/lib/ref.js @@ -125,6 +125,12 @@ $Ref.isExternal$Ref = function (value) { */ $Ref.isAllowed$Ref = function (value, options) { if ($Ref.is$Ref(value)) { + + // We might have a custom resolve decision to make + if (options && options.dereference && options.dereference.isRefResolved) { + return options.dereference.isRefResolved(value); + } + if (value.$ref.substr(0, 2) === "#/" || value.$ref === "#") { // It's a JSON Pointer reference, which is always allowed return true; diff --git a/test/specs/custom/bundled.js b/test/specs/custom/bundled.js new file mode 100644 index 00000000..8eaa29ac --- /dev/null +++ b/test/specs/custom/bundled.js @@ -0,0 +1,68 @@ +"use strict"; + +module.exports = +{ + definitions: { + requiredString: { + title: "requiredString", + minLength: 1, + type: "string" + }, + genderEnum: { + enum: [ + "male", + "female" + ], + type: "string" + }, + name: { + required: [ + "first", + "last" + ], + type: "object", + properties: { + first: { + $ref: "#/definitions/requiredString" + }, + last: { + $ref: "#/definitions/requiredString" + }, + middle: { + type: { + $ref: "#/definitions/requiredString/type" + }, + minLength: { + $ref: "#/definitions/requiredString/minLength" + } + }, + prefix: { + $ref: "#/definitions/requiredString", + minLength: 3 + }, + suffix: { + type: "string", + $ref: "#/definitions/name/properties/prefix", + maxLength: 3 + } + } + } + }, + required: [ + "name" + ], + type: "object", + properties: { + gender: { + $ref: "#/definitions/genderEnum" + }, + age: { + minimum: 0, + type: "integer" + }, + name: { + $ref: "#/definitions/name" + } + }, + title: "Person" +}; diff --git a/test/specs/custom/custom.spec.js b/test/specs/custom/custom.spec.js new file mode 100644 index 00000000..8e217b1c --- /dev/null +++ b/test/specs/custom/custom.spec.js @@ -0,0 +1,59 @@ +"use strict"; + +const { expect } = require("chai"); +const $RefParser = require("../../../lib"); +const helper = require("../../utils/helper"); +const path = require("../../utils/path"); +const parsedSchema = require("./parsed"); +const dereferencedSchema = require("./dereferenced"); +const bundledSchema = require("./bundled"); + +const options = { + dereference: { + circular: true, + + isRefResolved (value) { + + // don't resolve where $ref contains the word 'gender' + return value.$ref.match(/\/gender/g) === null; + } + } +}; + +describe("Schema with internal $refs (custom dereference)", () => { + it("should parse successfully", async () => { + let parser = new $RefParser(); + const schema = await parser.parse(path.rel("specs/custom/custom.yaml"), options); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(parsedSchema); + expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/custom/custom.yaml")]); + }); + + it("should resolve successfully", helper.testResolve( + path.rel("specs/custom/custom.yaml"), + path.abs("specs/custom/custom.yaml"), parsedSchema + )); + + it("should dereference successfully", async () => { + let parser = new $RefParser(); + const schema = await parser.dereference(path.rel("specs/custom/custom.yaml"), options); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(dereferencedSchema); + // Reference equality + expect(schema.properties.name).to.equal(schema.definitions.name); + expect(schema.definitions.requiredString) + .to.equal(schema.definitions.name.properties.first) + .to.equal(schema.definitions.name.properties.last) + .to.equal(schema.properties.name.properties.first) + .to.equal(schema.properties.name.properties.last); + // 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/custom/custom.yaml"), options); + expect(schema).to.equal(parser.schema); + expect(schema).to.deep.equal(bundledSchema); + }); +}); diff --git a/test/specs/custom/custom.yaml b/test/specs/custom/custom.yaml new file mode 100644 index 00000000..72c6dcbd --- /dev/null +++ b/test/specs/custom/custom.yaml @@ -0,0 +1,44 @@ +title: Person +type: object +definitions: + name: + type: object + required: + - first + - last + properties: + first: + $ref: "#/definitions/requiredString" + last: + $ref: "#/definitions/name/properties/first" + middle: + type: + $ref: "#/definitions/name/properties/first/type" + minLength: + $ref: "#/definitions/name/properties/last/minLength" + prefix: + $ref: "#/definitions/name/properties/last" + minLength: 3 + suffix: + type: string + $ref: "#/definitions/name/properties/prefix" + maxLength: 3 + requiredString: + title: requiredString + type: string + minLength: 1 + genderEnum: + type: string + enum: + - male + - female +required: + - name +properties: + name: + $ref: "#/definitions/name" + age: + type: integer + minimum: 0 + gender: + $ref: "#/definitions/genderEnum" diff --git a/test/specs/custom/dereferenced.js b/test/specs/custom/dereferenced.js new file mode 100644 index 00000000..484f1aa2 --- /dev/null +++ b/test/specs/custom/dereferenced.js @@ -0,0 +1,101 @@ +"use strict"; + +module.exports = +{ + definitions: { + requiredString: { + title: "requiredString", + minLength: 1, + type: "string" + }, + name: { + required: [ + "first", + "last" + ], + type: "object", + properties: { + first: { + title: "requiredString", + type: "string", + minLength: 1 + }, + last: { + title: "requiredString", + type: "string", + minLength: 1 + }, + middle: { + type: "string", + minLength: 1 + }, + prefix: { + title: "requiredString", + type: "string", + minLength: 3 + }, + suffix: { + title: "requiredString", + type: "string", + minLength: 3, + maxLength: 3 + } + } + }, + genderEnum: { + enum: [ + "male", + "female" + ], + type: "string", + } + }, + required: [ + "name" + ], + type: "object", + properties: { + gender: { + $ref: "#/definitions/genderEnum" + }, + age: { + minimum: 0, + type: "integer" + }, + name: { + required: [ + "first", + "last" + ], + type: "object", + properties: { + first: { + title: "requiredString", + type: "string", + minLength: 1 + }, + last: { + title: "requiredString", + type: "string", + minLength: 1 + }, + middle: { + type: "string", + minLength: 1 + }, + prefix: { + title: "requiredString", + type: "string", + minLength: 3 + }, + suffix: { + title: "requiredString", + type: "string", + minLength: 3, + maxLength: 3 + } + } + } + }, + title: "Person" +}; diff --git a/test/specs/custom/parsed.js b/test/specs/custom/parsed.js new file mode 100644 index 00000000..591020d0 --- /dev/null +++ b/test/specs/custom/parsed.js @@ -0,0 +1,68 @@ +"use strict"; + +module.exports = +{ + definitions: { + requiredString: { + title: "requiredString", + minLength: 1, + type: "string" + }, + name: { + required: [ + "first", + "last" + ], + type: "object", + properties: { + first: { + $ref: "#/definitions/requiredString" + }, + last: { + $ref: "#/definitions/name/properties/first" + }, + middle: { + type: { + $ref: "#/definitions/name/properties/first/type" + }, + minLength: { + $ref: "#/definitions/name/properties/last/minLength" + } + }, + prefix: { + $ref: "#/definitions/name/properties/last", + minLength: 3 + }, + suffix: { + type: "string", + $ref: "#/definitions/name/properties/prefix", + maxLength: 3 + } + } + }, + genderEnum: { + enum: [ + "male", + "female" + ], + type: "string", + } + }, + required: [ + "name" + ], + type: "object", + properties: { + gender: { + $ref: "#/definitions/genderEnum" + }, + age: { + minimum: 0, + type: "integer" + }, + name: { + $ref: "#/definitions/name" + } + }, + title: "Person" +};