Skip to content

Commit 889e69e

Browse files
committed
fix(ref): pass original ref
1 parent d6823e8 commit 889e69e

File tree

5 files changed

+142
-2
lines changed

5 files changed

+142
-2
lines changed

docs/plugins/file-info-object.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ The file info object currently only consists of a few properties, but it may gro
77
| Property | Type | Description |
88
| :---------- | :------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
| `url` | `string` | The full URL of the file. This could be any type of URL, including "http://", "https://", "file://", "ftp://", "mongodb://", or even a local filesystem path (when running in Node.js). |
10+
| `reference` | `string` | The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash. This is useful for custom resolvers that need the original relative reference. |
11+
| `baseUrl` | `string` | The URL that `reference` was resolved against to produce `url`. This may include a JSON Pointer fragment when the reference came from a nested location. |
1012
| `extension` | `string` | The lowercase file extension, such as ".json", ".yaml", ".txt", etc. |
1113
| `data` | `string` [`Buffer`](https://nodejs.org/api/buffer.html#buffer_buffer) etc. | The raw file contents, in whatever form they were returned by the [resolver](resolvers.md) that read the file. |

lib/parse.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ import type $Refs from "./refs.js";
1212
import type { ParserOptions } from "./options.js";
1313
import type { FileInfo, JSONSchema } from "./types/index.js";
1414

15+
interface ParseTarget {
16+
url: string;
17+
reference?: string;
18+
baseUrl?: string;
19+
}
20+
1521
/**
1622
* Reads and parses the specified file path or URL.
1723
*/
1824
async function parse<S extends object = JSONSchema, O extends ParserOptions<S> = ParserOptions<S>>(
19-
path: string,
25+
target: string | ParseTarget,
2026
$refs: $Refs<S, O>,
2127
options: O,
2228
) {
29+
let path = typeof target === "string" ? target : target.url;
30+
const baseUrl = typeof target === "string" ? undefined : target.baseUrl;
31+
let reference = typeof target === "string" ? undefined : target.reference;
32+
2333
// Remove the URL fragment, if any
2434
const hashIndex = path.indexOf("#");
2535
let hash = "";
@@ -28,6 +38,12 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
2838
// Remove the URL fragment, if any
2939
path = path.substring(0, hashIndex);
3040
}
41+
if (reference) {
42+
const referenceHashIndex = reference.indexOf("#");
43+
if (referenceHashIndex >= 0) {
44+
reference = reference.substring(0, referenceHashIndex);
45+
}
46+
}
3147

3248
// Add a new $Ref for this file, even though we don't have the value yet.
3349
// This ensures that we don't simultaneously read & parse the same file multiple times
@@ -38,6 +54,8 @@ async function parse<S extends object = JSONSchema, O extends ParserOptions<S> =
3854
url: path,
3955
hash,
4056
extension: url.getExtension(path),
57+
...(reference !== undefined ? { reference } : {}),
58+
...(baseUrl !== undefined ? { baseUrl } : {}),
4159
} as FileInfo;
4260

4361
// Read the file and then parse the data

lib/resolve-external.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,19 @@ async function resolve$Ref<S extends object = JSONSchema, O extends ParserOption
129129

130130
// Parse the $referenced file/url
131131
try {
132-
const result = await parse(resolvedPath, $refs, options);
132+
const reference = ($ref as JSONSchema).$ref;
133+
const parseTarget: { url: string; baseUrl: string; reference?: string } = {
134+
url: resolvedPath,
135+
baseUrl: resolutionBase,
136+
};
137+
if (typeof reference === "string") {
138+
parseTarget.reference = reference;
139+
}
140+
const result = await parse(
141+
parseTarget,
142+
$refs,
143+
options,
144+
);
133145

134146
// Crawl the parsed value
135147
// console.log('Resolving $ref pointers in %s', withoutHash);

lib/types/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ export interface FileInfo {
139139
*/
140140
url: string;
141141

142+
/**
143+
* The unresolved `$ref` value exactly as it appeared in the parent schema, without the hash.
144+
* This is useful for custom resolvers that need to preserve relative reference semantics instead
145+
* of relying on the already-resolved `url`.
146+
*/
147+
reference?: string;
148+
149+
/**
150+
* The URL that `reference` was resolved against to produce `url`.
151+
* This may include a JSON Pointer fragment when the reference originated from a nested location.
152+
*/
153+
baseUrl?: string;
154+
142155
/**
143156
* The hash (URL fragment) of the file URL, including the # symbol. If the URL doesn't have a hash, then this will be an empty string.
144157
*/

test/specs/resolvers/resolvers.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,101 @@ describe("options.resolve", () => {
198198
expect(schema).to.deep.equal(dereferencedSchema);
199199
});
200200

201+
it("should expose the original relative $ref to custom resolvers", async () => {
202+
const rootPath = path.abs("test/specs/resolvers/resolvers.yaml");
203+
const rootUrl = path.abs("test/specs/resolvers/resolvers.yaml");
204+
const petUrl = path.abs("test/specs/resolvers/definitions/pet.yaml");
205+
let canReadInfo: Pick<FileInfo, "url" | "reference" | "baseUrl" | "hash"> | undefined;
206+
let readInfo: Pick<FileInfo, "url" | "reference" | "baseUrl" | "hash"> | undefined;
207+
208+
const schema = await $RefParser.dereference(
209+
rootPath,
210+
{
211+
type: "object",
212+
properties: {
213+
pet: {
214+
$ref: "definitions/pet.yaml",
215+
},
216+
},
217+
},
218+
{
219+
resolve: {
220+
relativeFile: {
221+
order: 1,
222+
canRead(file: FileInfo) {
223+
canReadInfo = {
224+
url: file.url,
225+
reference: file.reference,
226+
baseUrl: file.baseUrl,
227+
hash: file.hash,
228+
};
229+
return file.reference === "definitions/pet.yaml";
230+
},
231+
async read(file: FileInfo) {
232+
readInfo = {
233+
url: file.url,
234+
reference: file.reference,
235+
baseUrl: file.baseUrl,
236+
hash: file.hash,
237+
};
238+
239+
return {
240+
title: "pet",
241+
type: "object",
242+
properties: {
243+
name: {
244+
type: "string",
245+
},
246+
age: {
247+
type: "number",
248+
},
249+
species: {
250+
type: "string",
251+
enum: ["cat", "dog", "bird", "fish"],
252+
},
253+
},
254+
};
255+
},
256+
},
257+
},
258+
},
259+
);
260+
261+
expect(canReadInfo).to.deep.equal({
262+
url: petUrl,
263+
reference: "definitions/pet.yaml",
264+
baseUrl: `${rootUrl}#/properties/pet`,
265+
hash: "",
266+
});
267+
expect(readInfo).to.deep.equal({
268+
url: petUrl,
269+
reference: "definitions/pet.yaml",
270+
baseUrl: `${rootUrl}#/properties/pet`,
271+
hash: "",
272+
});
273+
expect(schema).to.deep.equal({
274+
type: "object",
275+
properties: {
276+
pet: {
277+
title: "pet",
278+
type: "object",
279+
properties: {
280+
name: {
281+
type: "string",
282+
},
283+
age: {
284+
type: "number",
285+
},
286+
species: {
287+
type: "string",
288+
enum: ["cat", "dog", "bird", "fish"],
289+
},
290+
},
291+
},
292+
},
293+
});
294+
});
295+
201296
it("should use a custom resolver that calls a callback", async () => {
202297
const schema = await $RefParser.dereference(path.abs("test/specs/resolvers/resolvers.yaml"), {
203298
resolve: {

0 commit comments

Comments
 (0)