@apidevtools/json-schema-ref-parser 10.0.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/bundle.d.ts +10 -0
- package/dist/lib/bundle.js +265 -0
- package/dist/lib/dereference.d.ts +9 -0
- package/dist/lib/dereference.js +206 -0
- package/dist/lib/index.d.ts +206 -0
- package/dist/lib/index.js +223 -0
- package/dist/lib/normalize-args.d.ts +10 -0
- package/dist/lib/normalize-args.js +42 -0
- package/dist/lib/options.d.ts +77 -0
- package/dist/lib/options.js +119 -0
- package/dist/lib/parse.d.ts +8 -0
- package/dist/lib/parse.js +177 -0
- package/dist/lib/parsers/binary.d.ts +3 -0
- package/dist/lib/parsers/binary.js +35 -0
- package/dist/lib/parsers/json.d.ts +3 -0
- package/dist/lib/parsers/json.js +57 -0
- package/dist/lib/parsers/text.d.ts +3 -0
- package/dist/lib/parsers/text.js +42 -0
- package/dist/lib/parsers/yaml.d.ts +3 -0
- package/dist/lib/parsers/yaml.js +65 -0
- package/dist/lib/pointer.d.ts +87 -0
- package/dist/lib/pointer.js +256 -0
- package/dist/lib/ref.d.ts +181 -0
- package/dist/lib/ref.js +239 -0
- package/dist/lib/refs.d.ts +127 -0
- package/dist/lib/refs.js +223 -0
- package/dist/lib/resolve-external.d.ts +14 -0
- package/dist/lib/resolve-external.js +148 -0
- package/dist/lib/resolvers/file.d.ts +3 -0
- package/dist/lib/resolvers/file.js +76 -0
- package/dist/lib/resolvers/http.d.ts +3 -0
- package/dist/lib/resolvers/http.js +159 -0
- package/dist/lib/types/index.d.ts +104 -0
- package/dist/lib/types/index.js +2 -0
- package/dist/lib/util/errors.d.ts +50 -0
- package/dist/lib/util/errors.js +106 -0
- package/dist/lib/util/maybe.d.ts +3 -0
- package/dist/lib/util/maybe.js +24 -0
- package/dist/lib/util/next.d.ts +2 -0
- package/dist/lib/util/next.js +16 -0
- package/dist/lib/util/plugins.d.ts +36 -0
- package/dist/lib/util/plugins.js +144 -0
- package/dist/lib/util/url.d.ts +93 -0
- package/{cjs → dist/lib}/util/url.js +134 -102
- package/dist/vite.config.d.ts +2 -0
- package/dist/vite.config.js +23 -0
- package/lib/{bundle.js → bundle.ts} +105 -101
- package/lib/{dereference.js → dereference.ts} +113 -52
- package/lib/index.ts +413 -0
- package/lib/{normalize-args.js → normalize-args.ts} +7 -14
- package/lib/options.ts +202 -0
- package/lib/parse.ts +153 -0
- package/lib/parsers/binary.ts +39 -0
- package/lib/parsers/{json.js → json.ts} +9 -22
- package/lib/parsers/text.ts +46 -0
- package/lib/parsers/{yaml.js → yaml.ts} +15 -19
- package/lib/pointer.ts +296 -0
- package/lib/ref.ts +288 -0
- package/lib/refs.ts +238 -0
- package/lib/{resolve-external.js → resolve-external.ts} +39 -36
- package/lib/resolvers/file.ts +40 -0
- package/lib/resolvers/http.ts +136 -0
- package/lib/tsconfig.json +103 -0
- package/lib/types/index.ts +135 -0
- package/lib/util/errors.ts +141 -0
- package/lib/util/maybe.ts +22 -0
- package/lib/util/next.ts +13 -0
- package/lib/util/{plugins.js → plugins.ts} +58 -57
- package/lib/util/{url.js → url.ts} +64 -83
- package/package.json +44 -45
- package/cjs/bundle.js +0 -304
- package/cjs/dereference.js +0 -258
- package/cjs/index.js +0 -603
- package/cjs/normalize-args.js +0 -64
- package/cjs/options.js +0 -125
- package/cjs/package.json +0 -3
- package/cjs/parse.js +0 -338
- package/cjs/parsers/binary.js +0 -54
- package/cjs/parsers/json.js +0 -199
- package/cjs/parsers/text.js +0 -61
- package/cjs/parsers/yaml.js +0 -239
- package/cjs/pointer.js +0 -290
- package/cjs/ref.js +0 -333
- package/cjs/refs.js +0 -214
- package/cjs/resolve-external.js +0 -333
- package/cjs/resolvers/file.js +0 -106
- package/cjs/resolvers/http.js +0 -184
- package/cjs/util/errors.js +0 -401
- package/cjs/util/plugins.js +0 -159
- package/cjs/util/projectDir.cjs +0 -6
- package/lib/index.d.ts +0 -496
- package/lib/index.js +0 -290
- package/lib/options.js +0 -128
- package/lib/parse.js +0 -162
- package/lib/parsers/binary.js +0 -53
- package/lib/parsers/text.js +0 -64
- package/lib/pointer.js +0 -293
- package/lib/ref.js +0 -292
- package/lib/refs.js +0 -196
- package/lib/resolvers/file.js +0 -63
- package/lib/resolvers/http.js +0 -155
- package/lib/util/errors.js +0 -134
- package/lib/util/projectDir.cjs +0 -6
package/lib/parse.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { ono } from "@jsdevtools/ono";
|
|
2
|
+
import * as url from "./util/url.js";
|
|
3
|
+
import * as plugins from "./util/plugins.js";
|
|
4
|
+
import {
|
|
5
|
+
ResolverError,
|
|
6
|
+
ParserError,
|
|
7
|
+
UnmatchedParserError,
|
|
8
|
+
UnmatchedResolverError,
|
|
9
|
+
isHandledError,
|
|
10
|
+
} from "./util/errors.js";
|
|
11
|
+
import type $Refs from "./refs.js";
|
|
12
|
+
import type { Options } from "./options.js";
|
|
13
|
+
import type { FileInfo } from "./types/index.js";
|
|
14
|
+
|
|
15
|
+
export default parse;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads and parses the specified file path or URL.
|
|
19
|
+
*/
|
|
20
|
+
async function parse(path: string, $refs: $Refs, options: Options) {
|
|
21
|
+
// Remove the URL fragment, if any
|
|
22
|
+
path = url.stripHash(path);
|
|
23
|
+
|
|
24
|
+
// Add a new $Ref for this file, even though we don't have the value yet.
|
|
25
|
+
// This ensures that we don't simultaneously read & parse the same file multiple times
|
|
26
|
+
const $ref = $refs._add(path);
|
|
27
|
+
|
|
28
|
+
// This "file object" will be passed to all resolvers and parsers.
|
|
29
|
+
const file = {
|
|
30
|
+
url: path,
|
|
31
|
+
extension: url.getExtension(path),
|
|
32
|
+
} as FileInfo;
|
|
33
|
+
|
|
34
|
+
// Read the file and then parse the data
|
|
35
|
+
try {
|
|
36
|
+
const resolver = await readFile(file, options, $refs);
|
|
37
|
+
$ref.pathType = resolver.plugin.name;
|
|
38
|
+
file.data = resolver.result;
|
|
39
|
+
|
|
40
|
+
const parser = await parseFile(file, options, $refs);
|
|
41
|
+
$ref.value = parser.result;
|
|
42
|
+
|
|
43
|
+
return parser.result;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (isHandledError(err)) {
|
|
46
|
+
$ref.value = err;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads the given file, using the configured resolver plugins
|
|
55
|
+
*
|
|
56
|
+
* @param file - An object containing information about the referenced file
|
|
57
|
+
* @param file.url - The full URL of the referenced file
|
|
58
|
+
* @param file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
|
59
|
+
* @param options
|
|
60
|
+
*
|
|
61
|
+
* @returns
|
|
62
|
+
* The promise resolves with the raw file contents and the resolver that was used.
|
|
63
|
+
*/
|
|
64
|
+
async function readFile(file: FileInfo, options: Options, $refs: $Refs): Promise<any> {
|
|
65
|
+
// console.log('Reading %s', file.url);
|
|
66
|
+
|
|
67
|
+
// Find the resolvers that can read this file
|
|
68
|
+
let resolvers = plugins.all(options.resolve);
|
|
69
|
+
resolvers = plugins.filter(resolvers, "canRead", file);
|
|
70
|
+
|
|
71
|
+
// Run the resolvers, in order, until one of them succeeds
|
|
72
|
+
plugins.sort(resolvers);
|
|
73
|
+
try {
|
|
74
|
+
const data = await plugins.run(resolvers, "read", file, $refs);
|
|
75
|
+
return data;
|
|
76
|
+
} catch (err: any) {
|
|
77
|
+
if (!err && options.continueOnError) {
|
|
78
|
+
// No resolver could be matched
|
|
79
|
+
throw new UnmatchedResolverError(file.url);
|
|
80
|
+
} else if (!err || !("error" in err)) {
|
|
81
|
+
// Throw a generic, friendly error.
|
|
82
|
+
throw ono.syntax(`Unable to resolve $ref pointer "${file.url}"`);
|
|
83
|
+
}
|
|
84
|
+
// Throw the original error, if it's one of our own (user-friendly) errors.
|
|
85
|
+
else if (err.error instanceof ResolverError) {
|
|
86
|
+
throw err.error;
|
|
87
|
+
} else {
|
|
88
|
+
throw new ResolverError(err, file.url);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parses the given file's contents, using the configured parser plugins.
|
|
95
|
+
*
|
|
96
|
+
* @param file - An object containing information about the referenced file
|
|
97
|
+
* @param file.url - The full URL of the referenced file
|
|
98
|
+
* @param file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
|
99
|
+
* @param file.data - The file contents. This will be whatever data type was returned by the resolver
|
|
100
|
+
* @param options
|
|
101
|
+
*
|
|
102
|
+
* @returns
|
|
103
|
+
* The promise resolves with the parsed file contents and the parser that was used.
|
|
104
|
+
*/
|
|
105
|
+
async function parseFile(file: FileInfo, options: Options, $refs: $Refs) {
|
|
106
|
+
// console.log('Parsing %s', file.url);
|
|
107
|
+
|
|
108
|
+
// Find the parsers that can read this file type.
|
|
109
|
+
// If none of the parsers are an exact match for this file, then we'll try ALL of them.
|
|
110
|
+
// This handles situations where the file IS a supported type, just with an unknown extension.
|
|
111
|
+
const allParsers = plugins.all(options.parse);
|
|
112
|
+
const filteredParsers = plugins.filter(allParsers, "canParse", file);
|
|
113
|
+
const parsers = filteredParsers.length > 0 ? filteredParsers : allParsers;
|
|
114
|
+
|
|
115
|
+
// Run the parsers, in order, until one of them succeeds
|
|
116
|
+
plugins.sort(parsers);
|
|
117
|
+
try {
|
|
118
|
+
const parser = await plugins.run(parsers, "parse", file, $refs);
|
|
119
|
+
if (!parser.plugin.allowEmpty && isEmpty(parser.result)) {
|
|
120
|
+
throw ono.syntax(`Error parsing "${file.url}" as ${parser.plugin.name}. \nParsed value is empty`);
|
|
121
|
+
} else {
|
|
122
|
+
return parser;
|
|
123
|
+
}
|
|
124
|
+
} catch (err: any) {
|
|
125
|
+
if (!err && options.continueOnError) {
|
|
126
|
+
// No resolver could be matched
|
|
127
|
+
throw new UnmatchedParserError(file.url);
|
|
128
|
+
} else if (err && err.message && err.message.startsWith("Error parsing")) {
|
|
129
|
+
throw err;
|
|
130
|
+
} else if (!err || !("error" in err)) {
|
|
131
|
+
throw ono.syntax(`Unable to parse ${file.url}`);
|
|
132
|
+
} else if (err.error instanceof ParserError) {
|
|
133
|
+
throw err.error;
|
|
134
|
+
} else {
|
|
135
|
+
throw new ParserError(err.error.message, file.url);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Determines whether the parsed value is "empty".
|
|
142
|
+
*
|
|
143
|
+
* @param value
|
|
144
|
+
* @returns
|
|
145
|
+
*/
|
|
146
|
+
function isEmpty(value: any) {
|
|
147
|
+
return (
|
|
148
|
+
value === undefined ||
|
|
149
|
+
(typeof value === "object" && Object.keys(value).length === 0) ||
|
|
150
|
+
(typeof value === "string" && value.trim().length === 0) ||
|
|
151
|
+
(Buffer.isBuffer(value) && value.length === 0)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FileInfo } from "../types/index.js";
|
|
2
|
+
import type { Plugin } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
const BINARY_REGEXP = /\.(jpeg|jpg|gif|png|bmp|ico)$/i;
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
/**
|
|
8
|
+
* The order that this parser will run, in relation to other parsers.
|
|
9
|
+
*/
|
|
10
|
+
order: 400,
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether to allow "empty" files (zero bytes).
|
|
14
|
+
*/
|
|
15
|
+
allowEmpty: true,
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Determines whether this parser can parse a given file reference.
|
|
19
|
+
* Parsers that return true will be tried, in order, until one successfully parses the file.
|
|
20
|
+
* Parsers that return false will be skipped, UNLESS all parsers returned false, in which case
|
|
21
|
+
* every parser will be tried.
|
|
22
|
+
*/
|
|
23
|
+
canParse(file: FileInfo) {
|
|
24
|
+
// Use this parser if the file is a Buffer, and has a known binary extension
|
|
25
|
+
return Buffer.isBuffer(file.data) && BINARY_REGEXP.test(file.url);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parses the given data as a Buffer (byte array).
|
|
30
|
+
*/
|
|
31
|
+
parse(file: FileInfo) {
|
|
32
|
+
if (Buffer.isBuffer(file.data)) {
|
|
33
|
+
return file.data;
|
|
34
|
+
} else {
|
|
35
|
+
// This will reject if data is anything other than a string or typed array
|
|
36
|
+
return Buffer.from(file.data);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
} as Plugin;
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { ParserError } from "../util/errors.js";
|
|
2
|
+
import type { FileInfo } from "../types/index.js";
|
|
3
|
+
import type { Plugin } from "../types/index.js";
|
|
2
4
|
|
|
3
5
|
export default {
|
|
4
6
|
/**
|
|
5
7
|
* The order that this parser will run, in relation to other parsers.
|
|
6
|
-
*
|
|
7
|
-
* @type {number}
|
|
8
8
|
*/
|
|
9
9
|
order: 100,
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Whether to allow "empty" files. This includes zero-byte files, as well as empty JSON objects.
|
|
13
|
-
*
|
|
14
|
-
* @type {boolean}
|
|
15
13
|
*/
|
|
16
14
|
allowEmpty: true,
|
|
17
15
|
|
|
@@ -20,21 +18,13 @@ export default {
|
|
|
20
18
|
* Parsers that match will be tried, in order, until one successfully parses the file.
|
|
21
19
|
* Parsers that don't match will be skipped, UNLESS none of the parsers match, in which case
|
|
22
20
|
* every parser will be tried.
|
|
23
|
-
*
|
|
24
|
-
* @type {RegExp|string|string[]|function}
|
|
25
21
|
*/
|
|
26
22
|
canParse: ".json",
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
25
|
* Parses the given file as JSON
|
|
30
|
-
*
|
|
31
|
-
* @param {object} file - An object containing information about the referenced file
|
|
32
|
-
* @param {string} file.url - The full URL of the referenced file
|
|
33
|
-
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
|
34
|
-
* @param {*} file.data - The file contents. This will be whatever data type was returned by the resolver
|
|
35
|
-
* @returns {Promise}
|
|
36
26
|
*/
|
|
37
|
-
async parse
|
|
27
|
+
async parse(file: FileInfo): Promise<object | undefined> {
|
|
38
28
|
let data = file.data;
|
|
39
29
|
if (Buffer.isBuffer(data)) {
|
|
40
30
|
data = data.toString();
|
|
@@ -43,19 +33,16 @@ export default {
|
|
|
43
33
|
if (typeof data === "string") {
|
|
44
34
|
if (data.trim().length === 0) {
|
|
45
35
|
return; // This mirrors the YAML behavior
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
36
|
+
} else {
|
|
48
37
|
try {
|
|
49
38
|
return JSON.parse(data);
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
39
|
+
} catch (e: any) {
|
|
52
40
|
throw new ParserError(e.message, file.url);
|
|
53
41
|
}
|
|
54
42
|
}
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
43
|
+
} else {
|
|
57
44
|
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
|
|
58
|
-
return data;
|
|
45
|
+
return data as object;
|
|
59
46
|
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
47
|
+
},
|
|
48
|
+
} as Plugin;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ParserError } from "../util/errors.js";
|
|
2
|
+
import type { FileInfo } from "../types/index.js";
|
|
3
|
+
import type { Plugin } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
const TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i;
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
/**
|
|
9
|
+
* The order that this parser will run, in relation to other parsers.
|
|
10
|
+
*/
|
|
11
|
+
order: 300,
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to allow "empty" files (zero bytes).
|
|
15
|
+
*/
|
|
16
|
+
allowEmpty: true,
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The encoding that the text is expected to be in.
|
|
20
|
+
*/
|
|
21
|
+
encoding: "utf8" as BufferEncoding,
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Determines whether this parser can parse a given file reference.
|
|
25
|
+
* Parsers that return true will be tried, in order, until one successfully parses the file.
|
|
26
|
+
* Parsers that return false will be skipped, UNLESS all parsers returned false, in which case
|
|
27
|
+
* every parser will be tried.
|
|
28
|
+
*/
|
|
29
|
+
canParse(file: FileInfo) {
|
|
30
|
+
// Use this parser if the file is a string or Buffer, and has a known text-based extension
|
|
31
|
+
return (typeof file.data === "string" || Buffer.isBuffer(file.data)) && TEXT_REGEXP.test(file.url);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses the given file as text
|
|
36
|
+
*/
|
|
37
|
+
parse(file: FileInfo) {
|
|
38
|
+
if (typeof file.data === "string") {
|
|
39
|
+
return file.data;
|
|
40
|
+
} else if (Buffer.isBuffer(file.data)) {
|
|
41
|
+
return file.data.toString(this.encoding);
|
|
42
|
+
} else {
|
|
43
|
+
throw new ParserError("data is not text", file.url);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
} as Plugin;
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { ParserError } from "../util/errors.js";
|
|
2
2
|
import yaml from "js-yaml";
|
|
3
3
|
import { JSON_SCHEMA } from "js-yaml";
|
|
4
|
+
import type { FileInfo } from "../types/index.js";
|
|
5
|
+
import type { Plugin } from "../types/index.js";
|
|
4
6
|
|
|
5
7
|
export default {
|
|
6
8
|
/**
|
|
7
9
|
* The order that this parser will run, in relation to other parsers.
|
|
8
|
-
*
|
|
9
|
-
* @type {number}
|
|
10
10
|
*/
|
|
11
11
|
order: 200,
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Whether to allow "empty" files. This includes zero-byte files, as well as empty JSON objects.
|
|
15
|
-
*
|
|
16
|
-
* @type {boolean}
|
|
17
15
|
*/
|
|
18
16
|
allowEmpty: true,
|
|
19
17
|
|
|
@@ -22,21 +20,20 @@ export default {
|
|
|
22
20
|
* Parsers that match will be tried, in order, until one successfully parses the file.
|
|
23
21
|
* Parsers that don't match will be skipped, UNLESS none of the parsers match, in which case
|
|
24
22
|
* every parser will be tried.
|
|
25
|
-
*
|
|
26
|
-
* @type {RegExp|string[]|function}
|
|
27
23
|
*/
|
|
28
|
-
canParse: [".yaml", ".yml", ".json"],
|
|
24
|
+
canParse: [".yaml", ".yml", ".json"], // JSON is valid YAML
|
|
29
25
|
|
|
30
26
|
/**
|
|
31
27
|
* Parses the given file as YAML
|
|
32
28
|
*
|
|
33
|
-
* @param
|
|
34
|
-
* @param
|
|
35
|
-
* @param
|
|
36
|
-
* @param
|
|
37
|
-
* @returns
|
|
29
|
+
* @param file - An object containing information about the referenced file
|
|
30
|
+
* @param file.url - The full URL of the referenced file
|
|
31
|
+
* @param file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
|
|
32
|
+
* @param file.data - The file contents. This will be whatever data type was returned by the resolver
|
|
33
|
+
* @returns
|
|
38
34
|
*/
|
|
39
|
-
async parse
|
|
35
|
+
async parse(file: FileInfo) {
|
|
36
|
+
// eslint-disable-line require-await
|
|
40
37
|
let data = file.data;
|
|
41
38
|
if (Buffer.isBuffer(data)) {
|
|
42
39
|
data = data.toString();
|
|
@@ -45,14 +42,13 @@ export default {
|
|
|
45
42
|
if (typeof data === "string") {
|
|
46
43
|
try {
|
|
47
44
|
return yaml.load(data, { schema: JSON_SCHEMA });
|
|
48
|
-
}
|
|
49
|
-
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// @ts-expect-error TS(2571): Object is of type 'unknown'.
|
|
50
47
|
throw new ParserError(e.message, file.url);
|
|
51
48
|
}
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
49
|
+
} else {
|
|
54
50
|
// data is already a JavaScript value (object, array, number, null, NaN, etc.)
|
|
55
51
|
return data;
|
|
56
52
|
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
53
|
+
},
|
|
54
|
+
} as Plugin;
|
package/lib/pointer.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type $RefParserOptions from "./options.js";
|
|
2
|
+
|
|
3
|
+
import $Ref from "./ref.js";
|
|
4
|
+
import * as url from "./util/url.js";
|
|
5
|
+
import { JSONParserError, InvalidPointerError, MissingPointerError, isHandledError } from "./util/errors.js";
|
|
6
|
+
const slashes = /\//g;
|
|
7
|
+
const tildes = /~/g;
|
|
8
|
+
const escapedSlash = /~1/g;
|
|
9
|
+
const escapedTilde = /~0/g;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This class represents a single JSON pointer and its resolved value.
|
|
13
|
+
*
|
|
14
|
+
* @param $ref
|
|
15
|
+
* @param path
|
|
16
|
+
* @param [friendlyPath] - The original user-specified path (used for error messages)
|
|
17
|
+
* @class
|
|
18
|
+
*/
|
|
19
|
+
class Pointer {
|
|
20
|
+
/**
|
|
21
|
+
* The {@link $Ref} object that contains this {@link Pointer} object.
|
|
22
|
+
*/
|
|
23
|
+
$ref: $Ref;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The file path or URL, containing the JSON pointer in the hash.
|
|
27
|
+
* This path is relative to the path of the main JSON schema file.
|
|
28
|
+
*/
|
|
29
|
+
path: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The original path or URL, used for error messages.
|
|
33
|
+
*/
|
|
34
|
+
originalPath: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The value of the JSON pointer.
|
|
38
|
+
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
value: any;
|
|
42
|
+
/**
|
|
43
|
+
* Indicates whether the pointer references itself.
|
|
44
|
+
*/
|
|
45
|
+
circular: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The number of indirect references that were traversed to resolve the value.
|
|
48
|
+
* Resolving a single pointer may require resolving multiple $Refs.
|
|
49
|
+
*/
|
|
50
|
+
indirections: number;
|
|
51
|
+
|
|
52
|
+
constructor($ref: any, path: any, friendlyPath: any) {
|
|
53
|
+
this.$ref = $ref;
|
|
54
|
+
|
|
55
|
+
this.path = path;
|
|
56
|
+
|
|
57
|
+
this.originalPath = friendlyPath || path;
|
|
58
|
+
|
|
59
|
+
this.value = undefined;
|
|
60
|
+
|
|
61
|
+
this.circular = false;
|
|
62
|
+
|
|
63
|
+
this.indirections = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the value of a nested property within the given object.
|
|
68
|
+
*
|
|
69
|
+
* @param obj - The object that will be crawled
|
|
70
|
+
* @param options
|
|
71
|
+
* @param pathFromRoot - the path of place that initiated resolving
|
|
72
|
+
*
|
|
73
|
+
* @returns
|
|
74
|
+
* Returns a JSON pointer whose {@link Pointer#value} is the resolved value.
|
|
75
|
+
* If resolving this value required resolving other JSON references, then
|
|
76
|
+
* the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
|
|
77
|
+
* of the resolved value.
|
|
78
|
+
*/
|
|
79
|
+
resolve(obj: any, options: any, pathFromRoot: any) {
|
|
80
|
+
const tokens = Pointer.parse(this.path, this.originalPath);
|
|
81
|
+
|
|
82
|
+
// Crawl the object, one token at a time
|
|
83
|
+
this.value = unwrapOrThrow(obj);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
86
|
+
if (resolveIf$Ref(this, options)) {
|
|
87
|
+
// The $ref path has changed, so append the remaining tokens to the path
|
|
88
|
+
this.path = Pointer.join(this.path, tokens.slice(i));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof this.value === "object" && this.value !== null && "$ref" in this.value) {
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const token = tokens[i];
|
|
96
|
+
if (this.value[token] === undefined || this.value[token] === null) {
|
|
97
|
+
this.value = null;
|
|
98
|
+
throw new MissingPointerError(token, decodeURI(this.originalPath));
|
|
99
|
+
} else {
|
|
100
|
+
this.value = this.value[token];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Resolve the final value
|
|
105
|
+
if (!this.value || (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)) {
|
|
106
|
+
resolveIf$Ref(this, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sets the value of a nested property within the given object.
|
|
114
|
+
*
|
|
115
|
+
* @param obj - The object that will be crawled
|
|
116
|
+
* @param value - the value to assign
|
|
117
|
+
* @param options
|
|
118
|
+
*
|
|
119
|
+
* @returns
|
|
120
|
+
* Returns the modified object, or an entirely new object if the entire object is overwritten.
|
|
121
|
+
*/
|
|
122
|
+
set(obj: any, value: any, options?: $RefParserOptions) {
|
|
123
|
+
const tokens = Pointer.parse(this.path);
|
|
124
|
+
let token;
|
|
125
|
+
|
|
126
|
+
if (tokens.length === 0) {
|
|
127
|
+
// There are no tokens, replace the entire object with the new value
|
|
128
|
+
this.value = value;
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Crawl the object, one token at a time
|
|
133
|
+
this.value = unwrapOrThrow(obj);
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
136
|
+
resolveIf$Ref(this, options);
|
|
137
|
+
|
|
138
|
+
token = tokens[i];
|
|
139
|
+
if (this.value && this.value[token] !== undefined) {
|
|
140
|
+
// The token exists
|
|
141
|
+
this.value = this.value[token];
|
|
142
|
+
} else {
|
|
143
|
+
// The token doesn't exist, so create it
|
|
144
|
+
this.value = setValue(this, token, {});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Set the value of the final token
|
|
149
|
+
resolveIf$Ref(this, options);
|
|
150
|
+
token = tokens[tokens.length - 1];
|
|
151
|
+
setValue(this, token, value);
|
|
152
|
+
|
|
153
|
+
// Return the updated object
|
|
154
|
+
return obj;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parses a JSON pointer (or a path containing a JSON pointer in the hash)
|
|
159
|
+
* and returns an array of the pointer's tokens.
|
|
160
|
+
* (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
|
|
161
|
+
*
|
|
162
|
+
* The pointer is parsed according to RFC 6901
|
|
163
|
+
* {@link https://tools.ietf.org/html/rfc6901#section-3}
|
|
164
|
+
*
|
|
165
|
+
* @param path
|
|
166
|
+
* @param [originalPath]
|
|
167
|
+
* @returns
|
|
168
|
+
*/
|
|
169
|
+
static parse(path: string, originalPath?: string) {
|
|
170
|
+
// Get the JSON pointer from the path's hash
|
|
171
|
+
let pointer = url.getHash(path).substr(1);
|
|
172
|
+
|
|
173
|
+
// If there's no pointer, then there are no tokens,
|
|
174
|
+
// so return an empty array
|
|
175
|
+
if (!pointer) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Split into an array
|
|
180
|
+
pointer = pointer.split("/");
|
|
181
|
+
|
|
182
|
+
// Decode each part, according to RFC 6901
|
|
183
|
+
for (let i = 0; i < pointer.length; i++) {
|
|
184
|
+
pointer[i] = decodeURIComponent(pointer[i].replace(escapedSlash, "/").replace(escapedTilde, "~"));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (pointer[0] !== "") {
|
|
188
|
+
throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return pointer.slice(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates a JSON pointer path, by joining one or more tokens to a base path.
|
|
196
|
+
*
|
|
197
|
+
* @param base - The base path (e.g. "schema.json#/definitions/person")
|
|
198
|
+
* @param tokens - The token(s) to append (e.g. ["name", "first"])
|
|
199
|
+
* @returns
|
|
200
|
+
*/
|
|
201
|
+
static join(base: any, tokens: any) {
|
|
202
|
+
// Ensure that the base path contains a hash
|
|
203
|
+
if (base.indexOf("#") === -1) {
|
|
204
|
+
base += "#";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Append each token to the base path
|
|
208
|
+
tokens = Array.isArray(tokens) ? tokens : [tokens];
|
|
209
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
210
|
+
const token = tokens[i];
|
|
211
|
+
// Encode the token, according to RFC 6901
|
|
212
|
+
base += "/" + encodeURIComponent(token.replace(tildes, "~0").replace(slashes, "~1"));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return base;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* If the given pointer's {@link Pointer#value} is a JSON reference,
|
|
221
|
+
* then the reference is resolved and {@link Pointer#value} is replaced with the resolved value.
|
|
222
|
+
* In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the
|
|
223
|
+
* resolution path of the new value.
|
|
224
|
+
*
|
|
225
|
+
* @param pointer
|
|
226
|
+
* @param options
|
|
227
|
+
* @returns - Returns `true` if the resolution path changed
|
|
228
|
+
*/
|
|
229
|
+
function resolveIf$Ref(pointer: any, options: any) {
|
|
230
|
+
// Is the value a JSON reference? (and allowed?)
|
|
231
|
+
|
|
232
|
+
if ($Ref.isAllowed$Ref(pointer.value, options)) {
|
|
233
|
+
const $refPath = url.resolve(pointer.path, pointer.value.$ref);
|
|
234
|
+
|
|
235
|
+
if ($refPath === pointer.path) {
|
|
236
|
+
// The value is a reference to itself, so there's nothing to do.
|
|
237
|
+
pointer.circular = true;
|
|
238
|
+
} else {
|
|
239
|
+
const resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options);
|
|
240
|
+
if (resolved === null) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pointer.indirections += resolved.indirections + 1;
|
|
245
|
+
|
|
246
|
+
if ($Ref.isExtended$Ref(pointer.value)) {
|
|
247
|
+
// This JSON reference "extends" the resolved value, rather than simply pointing to it.
|
|
248
|
+
// So the resolved path does NOT change. Just the value does.
|
|
249
|
+
pointer.value = $Ref.dereference(pointer.value, resolved.value);
|
|
250
|
+
return false;
|
|
251
|
+
} else {
|
|
252
|
+
// Resolve the reference
|
|
253
|
+
pointer.$ref = resolved.$ref;
|
|
254
|
+
pointer.path = resolved.path;
|
|
255
|
+
pointer.value = resolved.value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export default Pointer;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Sets the specified token value of the {@link Pointer#value}.
|
|
266
|
+
*
|
|
267
|
+
* The token is evaluated according to RFC 6901.
|
|
268
|
+
* {@link https://tools.ietf.org/html/rfc6901#section-4}
|
|
269
|
+
*
|
|
270
|
+
* @param pointer - The JSON Pointer whose value will be modified
|
|
271
|
+
* @param token - A JSON Pointer token that indicates how to modify `obj`
|
|
272
|
+
* @param value - The value to assign
|
|
273
|
+
* @returns - Returns the assigned value
|
|
274
|
+
*/
|
|
275
|
+
function setValue(pointer: any, token: any, value: any) {
|
|
276
|
+
if (pointer.value && typeof pointer.value === "object") {
|
|
277
|
+
if (token === "-" && Array.isArray(pointer.value)) {
|
|
278
|
+
pointer.value.push(value);
|
|
279
|
+
} else {
|
|
280
|
+
pointer.value[token] = value;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
throw new JSONParserError(
|
|
284
|
+
`Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function unwrapOrThrow(value: any) {
|
|
291
|
+
if (isHandledError(value)) {
|
|
292
|
+
throw value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return value;
|
|
296
|
+
}
|