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

feat: dereference.preservedProperties for preserving data during dereferencing #369

Merged
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
1 change: 1 addition & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,4 @@ The `dereference` options control how JSON Schema $Ref Parser will dereference `
| `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. |
| `onCircular` | `(string) => void` | A function, called immediately after detecting a circular `$ref` with the circular `$ref` in question. |
| `onDereference` | `(string, JSONSchemaObjectType, JSONSchemaObjectType, string) => void` | A function, called immediately after dereferencing, with: the resolved JSON Schema value, the `$ref` being dereferenced, the object holding the dereferenced prop, the dereferenced prop name. |
| `preservedProperties` | `string[]` | An array of properties to preserve when dereferencing a `$ref` schema. Useful if you want to enforce non-standard dereferencing behavior like present in the OpenAPI 3.1 specification where `description` and `summary` properties are preserved when alongside a `$ref` pointer. |
24 changes: 24 additions & 0 deletions lib/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,31 @@ function crawl<S extends object = JSONSchema, O extends ParserOptions<S> = Parse
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
if (obj[key] !== dereferenced.value) {
// If we have properties we want to preserve from our dereferenced schema then we need
// to copy them over to our new object.
const preserved: Map<string, unknown> = new Map();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we wrap this logic and this object allocation inside a check for if derefOptions?.preservedProperties is set? if its not set we can skip a lot of this logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely!

if (derefOptions?.preservedProperties) {
if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
derefOptions?.preservedProperties.forEach((prop) => {
if (prop in obj[key]) {
preserved.set(prop, obj[key][prop]);
}
});
}
}

obj[key] = dereferenced.value;

// If we have data to preserve and our dereferenced object is still an object then
// we need copy back our preserved data into our dereferenced schema.
if (derefOptions?.preservedProperties) {
if (preserved.size && typeof obj[key] === "object" && !Array.isArray(obj[key])) {
preserved.forEach((value, prop) => {
obj[key][prop] = value;
});
}
}

derefOptions?.onDereference?.(value.$ref, obj[key], obj, key);
}
} else {
Expand Down
10 changes: 10 additions & 0 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ export interface DereferenceOptions {
*/
onDereference?(path: string, value: JSONSchemaObject, parent?: JSONSchemaObject, parentPropName?: string): void;

/**
* An array of properties to preserve when dereferencing a `$ref` schema. Useful if you want to
* enforce non-standard dereferencing behavior like present in the OpenAPI 3.1 specification where
* `description` and `summary` properties are preserved when alongside a `$ref` pointer.
*
* If none supplied then no properties will be preserved and the object will be fully replaced
* with the dereferenced `$ref`.
*/
preservedProperties?: string[];

/**
* Whether a reference should resolve relative to its directory/path, or from the cwd
*
Expand Down
2 changes: 1 addition & 1 deletion lib/util/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class MissingPointerError extends JSONParserError {
public targetToken: any;
public targetRef: string;
public targetFound: string;
public parentPath: string;
public parentPath: string;
constructor(token: any, path: any, targetRef: any, targetFound: any, parentPath: any) {
super(`Missing $ref pointer "${getHash(path)}". Token "${token}" does not exist.`, stripHash(path));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it } from "vitest";
import $RefParser from "../../../lib/index.js";
import pathUtils from "../../utils/path.js";

import { expect } from "vitest";
import type { Options } from "../../../lib/options";

describe("dereference.preservedProperties", () => {
it("should preserve properties", async () => {
const parser = new $RefParser();
const schema = pathUtils.rel("test/specs/dereference-preservedProperties/dereference-preservedProperties.yaml");
const options = {
dereference: {
preservedProperties: ["description"],
},
} as Options;
const res = await parser.dereference(schema, options);

expect(res).to.deep.equal({
title: "Person",
required: ["name"],
type: "object",
definitions: {
name: {
type: "string",
description: "Someone's name",
},
},
properties: {
name: {
type: "string",
description: "Someone's name",
},
secretName: {
type: "string",
description: "Someone's secret name",
},
},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
title: Person
required:
- name
type: object
definitions:
name:
type: string
description: Someone's name
properties:
name:
$ref: "#/definitions/name"
secretName:
$ref: "#/definitions/name"
# Despite "Someone's name" being the description of the referenced `name` schema our overwritten
# description should be preserved instead.
description: Someone's secret name