Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow excluding certain paths from dereferencing #249

Merged
merged 5 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ $RefParser.dereference("my-schema.yaml", {
}
},
dereference: {
circular: false // Don't allow circular $refs
circular: false, // Don't allow circular $refs
excludedPathMatcher: (path) => // Skip dereferencing content under any 'example' key
path.includes("/example/")
}
});
```
Expand Down Expand Up @@ -75,3 +77,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.<br><br>If set to `false`, then a `ReferenceError` will be thrown if the schema contains any circular references.<br><br> 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`.
|`excludedPathMatcher`|`(string) => boolean`|A function, called for each path, which can return true to stop this path and all subpaths from being dereferenced further. This is useful in schemas where some subpaths contain literal $ref keys that should not be dereferenced.
9 changes: 8 additions & 1 deletion lib/dereference.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ function crawl (obj, path, pathFromRoot, parents, processedObjects, dereferenced
circular: false
};

let isExcludedPath = options.dereference.excludedPathMatcher;

if (options.dereference.circular === "ignore" || !processedObjects.has(obj)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
parents.add(obj);
processedObjects.add(obj);

Expand All @@ -55,6 +57,11 @@ function crawl (obj, path, pathFromRoot, parents, processedObjects, dereferenced
for (const key of Object.keys(obj)) {
let keyPath = Pointer.join(path, key);
let keyPathFromRoot = Pointer.join(pathFromRoot, key);

if (isExcludedPath(keyPathFromRoot)) {
continue;
}

let value = obj[key];
let circular = false;

Expand Down
7 changes: 7 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ 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";

/**
* A function, called for each path, which can return true to stop this path and all
* subpaths from being dereferenced further. This is useful in schemas where some
* subpaths contain literal $ref keys that should not be dereferenced.
*/
excludedPathMatcher?(path: string): boolean;
};
}

Expand Down
11 changes: 10 additions & 1 deletion lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ $RefParserOptions.defaults = {
*
* @type {boolean|string}
*/
circular: true
circular: true,

/**
* A function, called for each path, which can return true to stop this path and all
* subpaths from being dereferenced further. This is useful in schemas where some
* subpaths contain literal $ref keys that should not be dereferenced.
*
* @type {function}
*/
excludedPathMatcher: () => false
},
};

Expand Down
110 changes: 110 additions & 0 deletions test/specs/ref-in-excluded-path/dereferenced.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use strict";

module.exports = {
components: {
examples: {
"confirmation-failure": {
value: {
$ref: "#/literal-component-example"
}
},
"confirmation-success": {
value: {
abc: "def"
}
},
"query-example": {
value: "abc"
}
},
parameters: {
a: {
example: {
$ref: "#/literal-param-component-example"
}
},
b: {
examples: {
example1: {
value: {
$ref: "#/literal-param-component-examples1"
}
}
}
}
}
},
paths: {
"/x/{id}": {
parameters: [
{
example: 123,
in: "path",
name: "id"
},
{
examples: {
e1: {
value: {
$ref: "#/literal-h1"
}
}
},
in: "header",
name: "h1"
},
{
example: {
$ref: "#/literal-q1"
},
in: "query",
name: "q1"
},
{
examples: {
q2: {
value: "abc"
}
},
in: "query",
name: "q2"
}
],
responses: {
200: {
content: {
"application/json": {
examples: {
"confirmation-failure": {
value: {
$ref: "#/literal-component-example"
}
},
"confirmation-in-progress": {
summary: "In progress response",
value: {
$ref: "#/abc"
}
},
"confirmation-success": {
value: {
abc: "def"
}
}
}
}
}
},
400: {
content: {
"application/json": {
example: {
$ref: "#/literal-example"
}
}
}
}
}
}
}
};
21 changes: 21 additions & 0 deletions test/specs/ref-in-excluded-path/ref-in-excluded-path.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use strict";

const { expect } = require("chai");
const $RefParser = require("../../..");
const path = require("../../utils/path");
const dereferencedSchema = require("./dereferenced");

describe("Schema with literal $refs in examples", () => {
it("should exclude the given paths from dereferencing", async () => {
let parser = new $RefParser();
const schema = await parser.dereference(path.rel("specs/ref-in-excluded-path/ref-in-excluded-path.yaml"), {
dereference: {
excludedPathMatcher: (schemaPath) => {
return /\/example(\/|$|s\/[^\/]+\/value(\/|$))/.test(schemaPath);
}
}
});
expect(schema).to.equal(parser.schema);
expect(schema).to.deep.equal(dereferencedSchema);
});
});
60 changes: 60 additions & 0 deletions test/specs/ref-in-excluded-path/ref-in-excluded-path.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Minimal parts of an OpenAPI 3.1.0 document, including various examples that should be literal:
paths:
'/x/{id}':
parameters:
- name: id
in: path
example:
123
- name: h1
in: header
examples:
e1:
value: # Literal value
$ref: '#/literal-h1'
- name: q1
in: query
example:
$ref: '#/literal-q1'
- name: q2
in: query
examples:
q2:
$ref: '#/components/examples/query-example'
responses:
'200':
content:
application/json:
examples:
confirmation-success: # Real ref
$ref: '#/components/examples/confirmation-success'
confirmation-in-progress:
summary: In progress response
value: # Literal ref! The $ref should not be dereferenced
$ref: '#/abc'
confirmation-failure: # Real ref to example with literal $ref
$ref: '#/components/examples/confirmation-failure'
'400':
content:
application/json:
example:
$ref: '#/literal-example'
components:
examples:
query-example:
value: abc
confirmation-success:
value:
abc: def
confirmation-failure:
value: # Literal value! The $ref should not be dereferenced
$ref: '#/literal-component-example'
parameters:
a:
example:
$ref: '#/literal-param-component-example'
b:
examples:
example1:
value:
$ref: '#/literal-param-component-examples1'