Skip to content

Commit

Permalink
feat: dereference.preservedProperties for preserving data during de…
Browse files Browse the repository at this point in the history
…referencing (#369)

* feat: `dereference.preservedProperties` for preserving data during dereferencing

* fix: typo

* fix: wrapping logic in a conditional
  • Loading branch information
erunion authored Jan 28, 2025
1 parent 9e2a1e6 commit ae7d95b
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 1 deletion.
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();
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

0 comments on commit ae7d95b

Please sign in to comment.