fork of hey-api/openapi-ts because I need some additional things
1import type { ParserOptions } from './options';
2import $Ref from './ref';
3import type { JSONSchema } from './types';
4import {
5 InvalidPointerError,
6 isHandledError,
7 JSONParserError,
8 MissingPointerError,
9} from './util/errors';
10import * as url from './util/url';
11
12const slashes = /\//g;
13const tildes = /~/g;
14const escapedSlash = /~1/g;
15const escapedTilde = /~0/g;
16
17const 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 */
33class 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 */
280function 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}
314export 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 */
327function 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
342function unwrapOrThrow(value: any) {
343 if (isHandledError(value)) {
344 throw value;
345 }
346
347 return value;
348}
349
350function isRootPath(pathFromRoot: any): boolean {
351 return typeof pathFromRoot == 'string' && Pointer.parse(pathFromRoot).length == 0;
352}