Skip to content

Commit c6d21ec

Browse files
committed
fix(null-ref): Fix references to existing but null values fail as missing #310
1 parent 325e34e commit c6d21ec

File tree

15 files changed

+136
-47
lines changed

15 files changed

+136
-47
lines changed

lib/bundle.ts

-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ function crawl(
4949
const obj = key === null ? parent : parent[key];
5050

5151
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
52-
// @ts-expect-error TS(2554): Expected 2 arguments, but got 1.
5352
if ($Ref.isAllowed$Ref(obj)) {
5453
inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
5554
} else {
@@ -76,7 +75,6 @@ function crawl(
7675
const keyPathFromRoot = Pointer.join(pathFromRoot, key);
7776
const value = obj[key];
7877

79-
// @ts-expect-error TS(2554): Expected 2 arguments, but got 1.
8078
if ($Ref.isAllowed$Ref(value)) {
8179
inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
8280
} else {

lib/parsers/yaml.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,8 @@ export default {
4141
if (typeof data === "string") {
4242
try {
4343
return yaml.load(data, { schema: JSON_SCHEMA });
44-
} catch (e) {
45-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
46-
throw new ParserError(e.message, file.url);
44+
} catch (e: any) {
45+
throw new ParserError(e?.message || "Parser Error", file.url);
4746
}
4847
} else {
4948
// data is already a JavaScript value (object, array, number, null, NaN, etc.)

lib/pointer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class Pointer {
4949
*/
5050
indirections: number;
5151

52-
constructor($ref: any, path: any, friendlyPath: any) {
52+
constructor($ref: any, path: any, friendlyPath?: string) {
5353
this.$ref = $ref;
5454

5555
this.path = path;
@@ -93,7 +93,7 @@ class Pointer {
9393
}
9494

9595
const token = tokens[i];
96-
if (this.value[token] === undefined || this.value[token] === null) {
96+
if (this.value[token] === undefined || (this.value[token] === null && i === tokens.length - 1)) {
9797
this.value = null;
9898
throw new MissingPointerError(token, decodeURI(this.originalPath));
9999
} else {
@@ -198,7 +198,7 @@ class Pointer {
198198
* @param tokens - The token(s) to append (e.g. ["name", "first"])
199199
* @returns
200200
*/
201-
static join(base: any, tokens: any) {
201+
static join(base: string, tokens: string | string[]) {
202202
// Ensure that the base path contains a hash
203203
if (base.indexOf("#") === -1) {
204204
base += "#";

lib/ref.ts

+17-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { InvalidPointerError, isHandledError, normalizeError } from "./util/erro
44
import { safePointerToPath, stripHash, getHash } from "./util/url.js";
55
import type $Refs from "./refs.js";
66
import type $RefParserOptions from "./options.js";
7+
import type { JSONSchema } from "./types";
78

89
export type $RefError = JSONParserError | ResolverError | ParserError | MissingPointerError;
910

@@ -86,7 +87,7 @@ class $Ref {
8687
* @param options
8788
* @returns
8889
*/
89-
exists(path: string, options: any) {
90+
exists(path: string, options?: $RefParserOptions) {
9091
try {
9192
this.resolve(path, options);
9293
return true;
@@ -102,7 +103,7 @@ class $Ref {
102103
* @param options
103104
* @returns - Returns the resolved value
104105
*/
105-
get(path: any, options: any) {
106+
get(path: any, options: $RefParserOptions) {
106107
return this.resolve(path, options)?.value;
107108
}
108109

@@ -144,8 +145,7 @@ class $Ref {
144145
* @param path - The full path of the property to set, optionally with a JSON pointer in the hash
145146
* @param value - The value to assign
146147
*/
147-
set(path: any, value: any) {
148-
// @ts-expect-error TS(2554): Expected 3 arguments, but got 2.
148+
set(path: string, value: any) {
149149
const pointer = new Pointer(this, path);
150150
this.value = pointer.set(this.value, value);
151151
}
@@ -156,8 +156,15 @@ class $Ref {
156156
* @param value - The value to inspect
157157
* @returns
158158
*/
159-
static is$Ref(value: any): value is { $ref: string; length?: number } {
160-
return value && typeof value === "object" && typeof value.$ref === "string" && value.$ref.length > 0;
159+
static is$Ref(value: unknown): value is { $ref: string; length?: number } {
160+
return (
161+
Boolean(value) &&
162+
typeof value === "object" &&
163+
value !== null &&
164+
"$ref" in value &&
165+
typeof value.$ref === "string" &&
166+
value.$ref.length > 0
167+
);
161168
}
162169

163170
/**
@@ -166,7 +173,7 @@ class $Ref {
166173
* @param value - The value to inspect
167174
* @returns
168175
*/
169-
static isExternal$Ref(value: any): boolean {
176+
static isExternal$Ref(value: unknown): boolean {
170177
return $Ref.is$Ref(value) && value.$ref![0] !== "#";
171178
}
172179

@@ -178,7 +185,7 @@ class $Ref {
178185
* @param options
179186
* @returns
180187
*/
181-
static isAllowed$Ref(value: any, options: any) {
188+
static isAllowed$Ref(value: unknown, options?: $RefParserOptions) {
182189
if (this.is$Ref(value)) {
183190
if (value.$ref.substring(0, 2) === "#/" || value.$ref === "#") {
184191
// It's a JSON Pointer reference, which is always allowed
@@ -224,7 +231,7 @@ class $Ref {
224231
* @param value - The value to inspect
225232
* @returns
226233
*/
227-
static isExtended$Ref(value: any) {
234+
static isExtended$Ref(value: unknown) {
228235
return $Ref.is$Ref(value) && Object.keys(value).length > 1;
229236
}
230237

@@ -259,7 +266,7 @@ class $Ref {
259266
* @param resolvedValue - The resolved value, which can be any type
260267
* @returns - Returns the dereferenced value
261268
*/
262-
static dereference($ref: $Ref, resolvedValue: any) {
269+
static dereference($ref: $Ref, resolvedValue: JSONSchema): JSONSchema {
263270
if (resolvedValue && typeof resolvedValue === "object" && $Ref.isExtended$Ref($ref)) {
264271
const merged = {};
265272
for (const key of Object.keys($ref)) {

lib/util/errors.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Ono } from "@jsdevtools/ono";
22
import { stripHash, toFileSystemPath } from "./url.js";
33
import type $RefParser from "../index.js";
4+
import type $Ref from "../ref";
45

56
export type JSONParserErrorType =
67
| "EUNKNOWN"
@@ -51,10 +52,8 @@ export class JSONParserErrorGroup extends Error {
5152
static getParserErrors(parser: any) {
5253
const errors = [];
5354

54-
for (const $ref of Object.values(parser.$refs._$refs)) {
55-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
55+
for (const $ref of Object.values(parser.$refs._$refs) as $Ref[]) {
5656
if ($ref.errors) {
57-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
5857
errors.push(...$ref.errors);
5958
}
6059
}

test/specs/error-source/error-source.spec.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ describe("Report correct error source and path for", () => {
1212
try {
1313
await parser.dereference({ foo: { bar: { $ref: "I do not exist" } } }, { continueOnError: true });
1414
helper.shouldNotGetCalled();
15-
} catch (err) {
16-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
15+
} catch (err: any) {
1716
expect(err.errors).to.containSubset([
1817
{
1918
name: ResolverError.name,
@@ -30,8 +29,7 @@ describe("Report correct error source and path for", () => {
3029
try {
3130
await parser.dereference(path.abs("test/specs/error-source/broken-external.json"), { continueOnError: true });
3231
helper.shouldNotGetCalled();
33-
} catch (err) {
34-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
32+
} catch (err: any) {
3533
expect(err.errors).to.containSubset([
3634
{
3735
name: ResolverError.name,
@@ -48,8 +46,7 @@ describe("Report correct error source and path for", () => {
4846
try {
4947
await parser.dereference(path.abs("test/specs/error-source/invalid-external.json"), { continueOnError: true });
5048
helper.shouldNotGetCalled();
51-
} catch (err) {
52-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
49+
} catch (err: any) {
5350
expect(err.errors).to.containSubset([
5451
{
5552
name: MissingPointerError.name,
@@ -72,8 +69,7 @@ describe("Report correct error source and path for", () => {
7269
try {
7370
await parser.dereference(path.abs("test/specs/error-source/invalid-pointer.json"), { continueOnError: true });
7471
helper.shouldNotGetCalled();
75-
} catch (err) {
76-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
72+
} catch (err: any) {
7773
expect(err.errors).to.containSubset([
7874
{
7975
name: InvalidPointerError.name,

test/specs/internal/internal.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ describe("Schema with internal $refs", () => {
2222
helper.testResolve(
2323
path.rel("test/specs/internal/internal.yaml"),
2424
path.abs("test/specs/internal/internal.yaml"),
25-
// @ts-expect-error TS(2554): Expected 2 arguments, but got 3.
2625
parsedSchema,
2726
),
2827
);

test/specs/invalid-pointers/invalid-pointers.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ describe("Schema with invalid pointers", () => {
1111
helper.shouldNotGetCalled();
1212
} catch (err) {
1313
expect(err).to.be.an.instanceOf(InvalidPointerError);
14-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
15-
expect(err.message).to.contain('Invalid $ref pointer "f". Pointers must begin with "#/"');
14+
expect((err as InvalidPointerError).message).to.contain(
15+
'Invalid $ref pointer "f". Pointers must begin with "#/"',
16+
);
1617
}
1718
});
1819

@@ -21,15 +22,13 @@ describe("Schema with invalid pointers", () => {
2122
try {
2223
await parser.dereference(path.rel("test/specs/invalid-pointers/invalid.json"), { continueOnError: true });
2324
helper.shouldNotGetCalled();
24-
} catch (err) {
25+
} catch (e) {
26+
const err = e as JSONParserErrorGroup;
2527
expect(err).to.be.instanceof(JSONParserErrorGroup);
26-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
2728
expect(err.files).to.equal(parser);
28-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
2929
expect(err.message).to.equal(
3030
`1 error occurred while reading '${path.abs("test/specs/invalid-pointers/invalid.json")}'`,
3131
);
32-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
3332
expect(err.errors).to.containSubset([
3433
{
3534
name: InvalidPointerError.name,

test/specs/no-refs/no-refs.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ describe("Schema without any $refs", () => {
1717

1818
it(
1919
"should resolve successfully",
20-
// @ts-expect-error TS(2554): Expected 2 arguments, but got 3.
2120
helper.testResolve(
2221
path.rel("test/specs/no-refs/no-refs.yaml"),
2322
path.abs("test/specs/no-refs/no-refs.yaml"),

test/specs/null-ref/null.spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, it } from "vitest";
2+
import { expect } from "vitest";
3+
import $RefParser from "../../../lib/index.js";
4+
import path from "../../utils/path";
5+
import parsedSchema from "./null";
6+
7+
describe("Null references", () => {
8+
it("should parse successfully", async () => {
9+
const parser = new $RefParser();
10+
const schema = await parser.parse(path.rel("test/specs/null-ref/null.yaml"));
11+
expect(schema).to.equal(parser.schema);
12+
expect(schema).to.deep.equal(parsedSchema);
13+
});
14+
});

test/specs/null-ref/null.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export default {
2+
openapi: "3.0.3",
3+
components: {
4+
schemas: {
5+
ReadyToPayMethods: {
6+
example: [
7+
{
8+
brands: ["Visa", "MasterCard"],
9+
feature: {
10+
name: "Google Pay",
11+
},
12+
filters: ["brand:Visa,MasterCard;bin:!411111"],
13+
method: "payment-card",
14+
},
15+
{
16+
feature: {
17+
expirationTime: "2006-01-02T15:04:05Z",
18+
linkToken: "some-random-link_token-for-plaid",
19+
name: "Plaid",
20+
},
21+
method: "ach",
22+
},
23+
{
24+
brands: ["Visa"],
25+
feature: null,
26+
filters: ["brand:Visa;bin:411111,444433"],
27+
method: "payment-card",
28+
},
29+
{
30+
feature: {
31+
$ref: "#/components/schemas/ReadyToPayMethods/example/2/feature",
32+
},
33+
filters: [],
34+
method: "ach",
35+
},
36+
{
37+
filters: [],
38+
method: "paypal",
39+
},
40+
{
41+
filters: [],
42+
method: "Skrill",
43+
},
44+
],
45+
type: "array",
46+
},
47+
},
48+
},
49+
};

test/specs/null-ref/null.yaml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
openapi: 3.0.3
2+
components:
3+
schemas:
4+
ReadyToPayMethods:
5+
example:
6+
- brands:
7+
- Visa
8+
- MasterCard
9+
feature:
10+
name: Google Pay
11+
filters:
12+
- brand:Visa,MasterCard;bin:!411111
13+
method: payment-card
14+
- feature:
15+
expirationTime: 2006-01-02T15:04:05Z
16+
linkToken: some-random-link_token-for-plaid
17+
name: Plaid
18+
method: ach
19+
- brands:
20+
- Visa
21+
feature: null
22+
filters:
23+
- brand:Visa;bin:411111,444433
24+
method: payment-card
25+
- feature:
26+
$ref: "#/components/schemas/ReadyToPayMethods/example/2/feature"
27+
filters: [ ]
28+
method: ach
29+
- filters: [ ]
30+
method: paypal
31+
- filters: [ ]
32+
method: Skrill
33+
type: array

test/specs/resolvers/resolvers.spec.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,7 @@ describe("options.resolve", () => {
176176
helper.shouldNotGetCalled();
177177
} catch (err) {
178178
expect(err).to.be.instanceof(ResolverError);
179-
// @ts-expect-error TS(2571): Object is of type 'unknown'.
180-
expect(err.message).to.contain("Error opening file");
179+
expect((err as ResolverError).message).to.contain("Error opening file");
181180
}
182181
});
183182

test/specs/root/root.spec.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ describe("Schema with a top-level (root) $ref", () => {
3838
expect(schema).to.equal(parser.schema);
3939
expect(schema).to.deep.equal(dereferencedSchema);
4040
// Reference equality
41-
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
42-
expect(schema.properties.first).to.deep.equal(schema.properties.last);
41+
expect(schema.properties!.first).to.deep.equal(schema.properties!.last);
4342
// The "circular" flag should NOT be set
4443
expect(parser.$refs.circular).to.equal(false);
4544
});

test/specs/substrings/substrings.spec.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ describe("$refs that are substrings of each other", () => {
3636
expect(schema).to.equal(parser.schema);
3737
expect(schema).to.deep.equal(dereferencedSchema);
3838
// Reference equality
39-
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
40-
expect(schema.properties.firstName).to.equal(schema.definitions.name);
41-
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
42-
expect(schema.properties.middleName).to.equal(schema.definitions["name-with-min-length"]);
43-
// @ts-expect-error TS(2532): Object is possibly 'undefined'.
44-
expect(schema.properties.lastName).to.equal(schema.definitions["name-with-min-length-max-length"]);
39+
const properties = schema.properties!;
40+
const definitions = schema.definitions!;
41+
expect(properties.firstName).to.equal(definitions.name);
42+
expect(properties.middleName).to.equal(definitions["name-with-min-length"]);
43+
expect(properties.lastName).to.equal(definitions["name-with-min-length-max-length"]);
4544
// The "circular" flag should NOT be set
4645
expect(parser.$refs.circular).to.equal(false);
4746
});

0 commit comments

Comments
 (0)