@hey-api/json-schema-ref-parser 0.0.0-next-20260212230650

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/src/parse.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { ono } from '@jsdevtools/ono';
2
+
3
+ import type { $RefParserOptions } from './options';
4
+ import type { FileInfo } from './types';
5
+ import { ParserError } from './util/errors';
6
+ import type { PluginResult } from './util/plugins';
7
+ import * as plugins from './util/plugins';
8
+ import { getExtension } from './util/url';
9
+
10
+ /**
11
+ * Prepares the file object so we can populate it with data and other values
12
+ * when it's read and parsed. This "file object" will be passed to all
13
+ * resolvers and parsers.
14
+ */
15
+ export function newFile(path: string): FileInfo {
16
+ let url = path;
17
+ // Remove the URL fragment, if any
18
+ const hashIndex = url.indexOf('#');
19
+ let hash = '';
20
+ if (hashIndex > -1) {
21
+ hash = url.substring(hashIndex);
22
+ url = url.substring(0, hashIndex);
23
+ }
24
+ return {
25
+ extension: getExtension(url),
26
+ hash,
27
+ url,
28
+ } as FileInfo;
29
+ }
30
+
31
+ /**
32
+ * Parses the given file's contents, using the configured parser plugins.
33
+ */
34
+ export const parseFile = async (
35
+ file: FileInfo,
36
+ options: $RefParserOptions,
37
+ ): Promise<PluginResult> => {
38
+ try {
39
+ // If none of the parsers are a match for this file, try all of them. This
40
+ // handles situations where the file is a supported type, just with an
41
+ // unknown extension.
42
+ const parsers = [
43
+ options.parse.json,
44
+ options.parse.yaml,
45
+ options.parse.text,
46
+ options.parse.binary,
47
+ ];
48
+ const filtered = parsers.filter((plugin) => plugin.canHandle(file));
49
+ return await plugins.run(filtered.length ? filtered : parsers, file);
50
+ } catch (error: any) {
51
+ if (error && error.message && error.message.startsWith('Error parsing')) {
52
+ throw error;
53
+ }
54
+
55
+ if (!error || !('error' in error)) {
56
+ throw ono.syntax(`Unable to parse ${file.url}`);
57
+ }
58
+
59
+ if (error.error instanceof ParserError) {
60
+ throw error.error;
61
+ }
62
+
63
+ throw new ParserError(error.error.message, file.url);
64
+ }
65
+ };
@@ -0,0 +1,13 @@
1
+ import type { FileInfo, Plugin } from '../types';
2
+
3
+ const BINARY_REGEXP = /\.(jpeg|jpg|gif|png|bmp|ico)$/i;
4
+
5
+ export const binaryParser: Plugin = {
6
+ canHandle: (file: FileInfo) => Buffer.isBuffer(file.data) && BINARY_REGEXP.test(file.url),
7
+ handler: (file: FileInfo): Buffer =>
8
+ Buffer.isBuffer(file.data)
9
+ ? file.data
10
+ : // This will reject if data is anything other than a string or typed array
11
+ Buffer.from(file.data),
12
+ name: 'binary',
13
+ };
@@ -0,0 +1,38 @@
1
+ import type { FileInfo, Plugin } from '../types';
2
+ import { ParserError } from '../util/errors';
3
+
4
+ export const jsonParser: Plugin = {
5
+ canHandle: (file: FileInfo) => file.extension === '.json',
6
+ async handler(file: FileInfo): Promise<object | undefined> {
7
+ let data = file.data;
8
+ if (Buffer.isBuffer(data)) {
9
+ data = data.toString();
10
+ }
11
+
12
+ if (typeof data !== 'string') {
13
+ // data is already a JavaScript value (object, array, number, null, NaN, etc.)
14
+ return data as object;
15
+ }
16
+
17
+ if (!data.trim().length) {
18
+ // this mirrors the YAML behavior
19
+ return;
20
+ }
21
+
22
+ try {
23
+ return JSON.parse(data);
24
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ } catch (error: any) {
26
+ try {
27
+ // find the first curly brace
28
+ const firstCurlyBrace = data.indexOf('{');
29
+ // remove any characters before the first curly brace
30
+ data = data.slice(firstCurlyBrace);
31
+ return JSON.parse(data);
32
+ } catch (error: any) {
33
+ throw new ParserError(error.message, file.url);
34
+ }
35
+ }
36
+ },
37
+ name: 'json',
38
+ };
@@ -0,0 +1,21 @@
1
+ import type { FileInfo, Plugin } from '../types';
2
+ import { ParserError } from '../util/errors';
3
+
4
+ const TEXT_REGEXP = /\.(txt|htm|html|md|xml|js|min|map|css|scss|less|svg)$/i;
5
+
6
+ export const textParser: Plugin = {
7
+ canHandle: (file: FileInfo) =>
8
+ (typeof file.data === 'string' || Buffer.isBuffer(file.data)) && TEXT_REGEXP.test(file.url),
9
+ handler(file: FileInfo): string {
10
+ if (typeof file.data === 'string') {
11
+ return file.data;
12
+ }
13
+
14
+ if (!Buffer.isBuffer(file.data)) {
15
+ throw new ParserError('data is not text', file.url);
16
+ }
17
+
18
+ return file.data.toString('utf-8');
19
+ },
20
+ name: 'text',
21
+ };
@@ -0,0 +1,26 @@
1
+ import yaml from 'js-yaml';
2
+ import { JSON_SCHEMA } from 'js-yaml';
3
+
4
+ import type { FileInfo, JSONSchema, Plugin } from '../types';
5
+ import { ParserError } from '../util/errors';
6
+
7
+ export const yamlParser: Plugin = {
8
+ // JSON is valid YAML
9
+ canHandle: (file: FileInfo) => ['.yaml', '.yml', '.json'].includes(file.extension),
10
+ handler: async (file: FileInfo): Promise<JSONSchema> => {
11
+ const data = Buffer.isBuffer(file.data) ? file.data.toString() : file.data;
12
+
13
+ if (typeof data !== 'string') {
14
+ // data is already a JavaScript value (object, array, number, null, NaN, etc.)
15
+ return data;
16
+ }
17
+
18
+ try {
19
+ const yamlSchema = yaml.load(data, { schema: JSON_SCHEMA }) as JSONSchema;
20
+ return yamlSchema;
21
+ } catch (error: any) {
22
+ throw new ParserError(error?.message || 'Parser Error', file.url);
23
+ }
24
+ },
25
+ name: 'yaml',
26
+ };
package/src/pointer.ts ADDED
@@ -0,0 +1,352 @@
1
+ import type { ParserOptions } from './options';
2
+ import $Ref from './ref';
3
+ import type { JSONSchema } from './types';
4
+ import {
5
+ InvalidPointerError,
6
+ isHandledError,
7
+ JSONParserError,
8
+ MissingPointerError,
9
+ } from './util/errors';
10
+ import * as url from './util/url';
11
+
12
+ const slashes = /\//g;
13
+ const tildes = /~/g;
14
+ const escapedSlash = /~1/g;
15
+ const escapedTilde = /~0/g;
16
+
17
+ const safeDecodeURIComponent = (encodedURIComponent: string): string => {
18
+ try {
19
+ return decodeURIComponent(encodedURIComponent);
20
+ } catch {
21
+ return encodedURIComponent;
22
+ }
23
+ };
24
+
25
+ /**
26
+ * This class represents a single JSON pointer and its resolved value.
27
+ *
28
+ * @param $ref
29
+ * @param path
30
+ * @param [friendlyPath] - The original user-specified path (used for error messages)
31
+ * @class
32
+ */
33
+ class Pointer<S extends object = JSONSchema> {
34
+ /**
35
+ * The {@link $Ref} object that contains this {@link Pointer} object.
36
+ */
37
+ $ref: $Ref<S>;
38
+
39
+ /**
40
+ * The file path or URL, containing the JSON pointer in the hash.
41
+ * This path is relative to the path of the main JSON schema file.
42
+ */
43
+ path: string;
44
+
45
+ /**
46
+ * The original path or URL, used for error messages.
47
+ */
48
+ originalPath: string;
49
+
50
+ /**
51
+ * The value of the JSON pointer.
52
+ * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
53
+ */
54
+
55
+ value: any;
56
+ /**
57
+ * Indicates whether the pointer references itself.
58
+ */
59
+ circular: boolean;
60
+ /**
61
+ * The number of indirect references that were traversed to resolve the value.
62
+ * Resolving a single pointer may require resolving multiple $Refs.
63
+ */
64
+ indirections: number;
65
+
66
+ constructor($ref: $Ref<S>, path: string, friendlyPath?: string) {
67
+ this.$ref = $ref;
68
+
69
+ this.path = path;
70
+
71
+ this.originalPath = friendlyPath || path;
72
+
73
+ this.value = undefined;
74
+
75
+ this.circular = false;
76
+
77
+ this.indirections = 0;
78
+ }
79
+
80
+ /**
81
+ * Resolves the value of a nested property within the given object.
82
+ *
83
+ * @param obj - The object that will be crawled
84
+ * @param options
85
+ * @param pathFromRoot - the path of place that initiated resolving
86
+ *
87
+ * @returns
88
+ * Returns a JSON pointer whose {@link Pointer#value} is the resolved value.
89
+ * If resolving this value required resolving other JSON references, then
90
+ * the {@link Pointer#$ref} and {@link Pointer#path} will reflect the resolution path
91
+ * of the resolved value.
92
+ */
93
+ resolve(obj: S, options?: ParserOptions, pathFromRoot?: string) {
94
+ const tokens = Pointer.parse(this.path, this.originalPath);
95
+
96
+ // Crawl the object, one token at a time
97
+ this.value = unwrapOrThrow(obj);
98
+
99
+ const errors: MissingPointerError[] = [];
100
+
101
+ for (let i = 0; i < tokens.length; i++) {
102
+ if (resolveIf$Ref(this, options, pathFromRoot)) {
103
+ // The $ref path has changed, so append the remaining tokens to the path
104
+ this.path = Pointer.join(this.path, tokens.slice(i));
105
+ }
106
+
107
+ if (
108
+ typeof this.value === 'object' &&
109
+ this.value !== null &&
110
+ !isRootPath(pathFromRoot) &&
111
+ '$ref' in this.value
112
+ ) {
113
+ return this;
114
+ }
115
+
116
+ const token = tokens[i]!;
117
+ if (
118
+ this.value[token] === undefined ||
119
+ (this.value[token] === null && i === tokens.length - 1)
120
+ ) {
121
+ // one final case is if the entry itself includes slashes, and was parsed out as a token - we can join the remaining tokens and try again
122
+ let didFindSubstringSlashMatch = false;
123
+ for (let j = tokens.length - 1; j > i; j--) {
124
+ const joinedToken = tokens.slice(i, j + 1).join('/');
125
+ if (this.value[joinedToken] !== undefined) {
126
+ this.value = this.value[joinedToken];
127
+ i = j;
128
+ didFindSubstringSlashMatch = true;
129
+ break;
130
+ }
131
+ }
132
+ if (didFindSubstringSlashMatch) {
133
+ continue;
134
+ }
135
+
136
+ this.value = null;
137
+ errors.push(new MissingPointerError(token, decodeURI(this.originalPath)));
138
+ } else {
139
+ this.value = this.value[token];
140
+ }
141
+ }
142
+
143
+ if (errors.length > 0) {
144
+ throw errors.length === 1
145
+ ? errors[0]
146
+ : new AggregateError(errors, 'Multiple missing pointer errors');
147
+ }
148
+
149
+ // Resolve the final value
150
+ if (
151
+ !this.value ||
152
+ (this.value.$ref && url.resolve(this.path, this.value.$ref) !== pathFromRoot)
153
+ ) {
154
+ resolveIf$Ref(this, options, pathFromRoot);
155
+ }
156
+
157
+ return this;
158
+ }
159
+
160
+ /**
161
+ * Sets the value of a nested property within the given object.
162
+ *
163
+ * @param obj - The object that will be crawled
164
+ * @param value - the value to assign
165
+ * @param options
166
+ *
167
+ * @returns
168
+ * Returns the modified object, or an entirely new object if the entire object is overwritten.
169
+ */
170
+ set(obj: S, value: any, options?: ParserOptions) {
171
+ const tokens = Pointer.parse(this.path);
172
+ let token;
173
+
174
+ if (tokens.length === 0) {
175
+ // There are no tokens, replace the entire object with the new value
176
+ this.value = value;
177
+ return value;
178
+ }
179
+
180
+ // Crawl the object, one token at a time
181
+ this.value = unwrapOrThrow(obj);
182
+
183
+ for (let i = 0; i < tokens.length - 1; i++) {
184
+ resolveIf$Ref(this, options);
185
+
186
+ token = tokens[i]!;
187
+ if (this.value && this.value[token] !== undefined) {
188
+ // The token exists
189
+ this.value = this.value[token];
190
+ } else {
191
+ // The token doesn't exist, so create it
192
+ this.value = setValue(this, token, {});
193
+ }
194
+ }
195
+
196
+ // Set the value of the final token
197
+ resolveIf$Ref(this, options);
198
+ token = tokens[tokens.length - 1];
199
+ setValue(this, token, value);
200
+
201
+ // Return the updated object
202
+ return obj;
203
+ }
204
+
205
+ /**
206
+ * Parses a JSON pointer (or a path containing a JSON pointer in the hash)
207
+ * and returns an array of the pointer's tokens.
208
+ * (e.g. "schema.json#/definitions/person/name" => ["definitions", "person", "name"])
209
+ *
210
+ * The pointer is parsed according to RFC 6901
211
+ * {@link https://tools.ietf.org/html/rfc6901#section-3}
212
+ *
213
+ * @param path
214
+ * @param [originalPath]
215
+ * @returns
216
+ */
217
+ static parse(path: string, originalPath?: string): string[] {
218
+ // Get the JSON pointer from the path's hash
219
+ const pointer = url.getHash(path).substring(1);
220
+
221
+ // If there's no pointer, then there are no tokens,
222
+ // so return an empty array
223
+ if (!pointer) {
224
+ return [];
225
+ }
226
+
227
+ // Split into an array
228
+ const split = pointer.split('/');
229
+
230
+ // Decode each part, according to RFC 6901
231
+ for (let i = 0; i < split.length; i++) {
232
+ split[i] = safeDecodeURIComponent(
233
+ split[i]!.replace(escapedSlash, '/').replace(escapedTilde, '~'),
234
+ );
235
+ }
236
+
237
+ if (split[0] !== '') {
238
+ throw new InvalidPointerError(pointer, originalPath === undefined ? path : originalPath);
239
+ }
240
+
241
+ return split.slice(1);
242
+ }
243
+
244
+ /**
245
+ * Creates a JSON pointer path, by joining one or more tokens to a base path.
246
+ *
247
+ * @param base - The base path (e.g. "schema.json#/definitions/person")
248
+ * @param tokens - The token(s) to append (e.g. ["name", "first"])
249
+ * @returns
250
+ */
251
+ static join(base: string, tokens: string | string[]) {
252
+ // Ensure that the base path contains a hash
253
+ if (base.indexOf('#') === -1) {
254
+ base += '#';
255
+ }
256
+
257
+ // Append each token to the base path
258
+ tokens = Array.isArray(tokens) ? tokens : [tokens];
259
+ for (let i = 0; i < tokens.length; i++) {
260
+ const token = tokens[i]!;
261
+ // Encode the token, according to RFC 6901
262
+ base += '/' + encodeURIComponent(token.replace(tildes, '~0').replace(slashes, '~1'));
263
+ }
264
+
265
+ return base;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * If the given pointer's {@link Pointer#value} is a JSON reference,
271
+ * then the reference is resolved and {@link Pointer#value} is replaced with the resolved value.
272
+ * In addition, {@link Pointer#path} and {@link Pointer#$ref} are updated to reflect the
273
+ * resolution path of the new value.
274
+ *
275
+ * @param pointer
276
+ * @param options
277
+ * @param [pathFromRoot] - the path of place that initiated resolving
278
+ * @returns - Returns `true` if the resolution path changed
279
+ */
280
+ function resolveIf$Ref(pointer: any, options: any, pathFromRoot?: any) {
281
+ // Is the value a JSON reference? (and allowed?)
282
+
283
+ if ($Ref.isAllowed$Ref(pointer.value)) {
284
+ const $refPath = url.resolve(pointer.path, pointer.value.$ref);
285
+
286
+ if ($refPath === pointer.path && !isRootPath(pathFromRoot)) {
287
+ // The value is a reference to itself, so there's nothing to do.
288
+ pointer.circular = true;
289
+ } else {
290
+ const resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options);
291
+ if (resolved === null) {
292
+ return false;
293
+ }
294
+
295
+ pointer.indirections += resolved.indirections + 1;
296
+
297
+ if ($Ref.isExtended$Ref(pointer.value)) {
298
+ // This JSON reference "extends" the resolved value, rather than simply pointing to it.
299
+ // So the resolved path does NOT change. Just the value does.
300
+ pointer.value = $Ref.dereference(pointer.value, resolved.value);
301
+ return false;
302
+ } else {
303
+ // Resolve the reference
304
+ pointer.$ref = resolved.$ref;
305
+ pointer.path = resolved.path;
306
+ pointer.value = resolved.value;
307
+ }
308
+
309
+ return true;
310
+ }
311
+ }
312
+ return undefined;
313
+ }
314
+ export default Pointer;
315
+
316
+ /**
317
+ * Sets the specified token value of the {@link Pointer#value}.
318
+ *
319
+ * The token is evaluated according to RFC 6901.
320
+ * {@link https://tools.ietf.org/html/rfc6901#section-4}
321
+ *
322
+ * @param pointer - The JSON Pointer whose value will be modified
323
+ * @param token - A JSON Pointer token that indicates how to modify `obj`
324
+ * @param value - The value to assign
325
+ * @returns - Returns the assigned value
326
+ */
327
+ function setValue(pointer: any, token: any, value: any) {
328
+ if (pointer.value && typeof pointer.value === 'object') {
329
+ if (token === '-' && Array.isArray(pointer.value)) {
330
+ pointer.value.push(value);
331
+ } else {
332
+ pointer.value[token] = value;
333
+ }
334
+ } else {
335
+ throw new JSONParserError(
336
+ `Error assigning $ref pointer "${pointer.path}". \nCannot set "${token}" of a non-object.`,
337
+ );
338
+ }
339
+ return value;
340
+ }
341
+
342
+ function unwrapOrThrow(value: any) {
343
+ if (isHandledError(value)) {
344
+ throw value;
345
+ }
346
+
347
+ return value;
348
+ }
349
+
350
+ function isRootPath(pathFromRoot: any): boolean {
351
+ return typeof pathFromRoot == 'string' && Pointer.parse(pathFromRoot).length == 0;
352
+ }