fork of hey-api/openapi-ts because I need some additional things
1import type { ParserOptions } from './options';
2import Pointer from './pointer';
3import type $Refs from './refs';
4import type { JSONSchema } from './types';
5import type {
6 JSONParserError,
7 MissingPointerError,
8 ParserError,
9 ResolverError,
10} from './util/errors';
11import { normalizeError } from './util/errors';
12
13export type $RefError = JSONParserError | ResolverError | ParserError | MissingPointerError;
14
15/**
16 * This class represents a single JSON reference and its resolved value.
17 *
18 * @class
19 */
20class $Ref<S extends object = JSONSchema> {
21 /**
22 * The file path or URL of the referenced file.
23 * This path is relative to the path of the main JSON schema file.
24 *
25 * This path does NOT contain document fragments (JSON pointers). It always references an ENTIRE file.
26 * Use methods such as {@link $Ref#get}, {@link $Ref#resolve}, and {@link $Ref#exists} to get
27 * specific JSON pointers within the file.
28 *
29 * @type {string}
30 */
31 path: undefined | string;
32
33 /**
34 * The resolved value of the JSON reference.
35 * Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
36 *
37 * @type {?*}
38 */
39 value: any;
40
41 /**
42 * The {@link $Refs} object that contains this {@link $Ref} object.
43 *
44 * @type {$Refs}
45 */
46 $refs: $Refs<S>;
47
48 /**
49 * Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
50 */
51 pathType: string | unknown;
52
53 /**
54 * List of all errors. Undefined if no errors.
55 */
56 errors: Array<$RefError> = [];
57
58 constructor($refs: $Refs<S>) {
59 this.$refs = $refs;
60 }
61
62 /**
63 * Pushes an error to errors array.
64 *
65 * @param err - The error to be pushed
66 * @returns
67 */
68 addError(err: $RefError) {
69 if (this.errors === undefined) {
70 this.errors = [];
71 }
72
73 const existingErrors = this.errors.map(({ footprint }: any) => footprint);
74
75 // the path has been almost certainly set at this point,
76 // but just in case something went wrong, normalizeError injects path if necessary
77 // moreover, certain errors might point at the same spot, so filter them out to reduce noise
78 if ('errors' in err && Array.isArray(err.errors)) {
79 this.errors.push(
80 ...err.errors
81 .map(normalizeError)
82 .filter(({ footprint }: any) => !existingErrors.includes(footprint)),
83 );
84 } else if (!('footprint' in err) || !existingErrors.includes(err.footprint)) {
85 this.errors.push(normalizeError(err));
86 }
87 }
88
89 /**
90 * Determines whether the given JSON reference exists within this {@link $Ref#value}.
91 *
92 * @param path - The full path being resolved, optionally with a JSON pointer in the hash
93 * @param options
94 * @returns
95 */
96 exists(path: string, options?: ParserOptions) {
97 try {
98 this.resolve(path, options);
99 return true;
100 } catch {
101 return false;
102 }
103 }
104
105 /**
106 * Resolves the given JSON reference within this {@link $Ref#value} and returns the resolved value.
107 *
108 * @param path - The full path being resolved, optionally with a JSON pointer in the hash
109 * @param options
110 * @returns - Returns the resolved value
111 */
112 get(path: string, options?: ParserOptions) {
113 return this.resolve(path, options)?.value;
114 }
115
116 /**
117 * Resolves the given JSON reference within this {@link $Ref#value}.
118 *
119 * @param path - The full path being resolved, optionally with a JSON pointer in the hash
120 * @param options
121 * @param friendlyPath - The original user-specified path (used for error messages)
122 * @param pathFromRoot - The path of `obj` from the schema root
123 * @returns
124 */
125 resolve(path: string, options?: ParserOptions, friendlyPath?: string, pathFromRoot?: string) {
126 const pointer = new Pointer<S>(this, path, friendlyPath);
127 return pointer.resolve(this.value, options, pathFromRoot);
128 }
129
130 /**
131 * Sets the value of a nested property within this {@link $Ref#value}.
132 * If the property, or any of its parents don't exist, they will be created.
133 *
134 * @param path - The full path of the property to set, optionally with a JSON pointer in the hash
135 * @param value - The value to assign
136 */
137 set(path: string, value: any) {
138 const pointer = new Pointer(this, path);
139 this.value = pointer.set(this.value, value);
140 }
141
142 /**
143 * Determines whether the given value is a JSON reference.
144 *
145 * @param value - The value to inspect
146 * @returns
147 */
148 static is$Ref(value: unknown): value is { $ref: string; length?: number } {
149 return (
150 Boolean(value) &&
151 typeof value === 'object' &&
152 value !== null &&
153 '$ref' in value &&
154 typeof value.$ref === 'string' &&
155 value.$ref.length > 0
156 );
157 }
158
159 /**
160 * Determines whether the given value is an external JSON reference.
161 *
162 * @param value - The value to inspect
163 * @returns
164 */
165 static isExternal$Ref(value: unknown): boolean {
166 return $Ref.is$Ref(value) && value.$ref![0] !== '#';
167 }
168
169 /**
170 * Determines whether the given value is a JSON reference, and whether it is allowed by the options.
171 *
172 * @param value - The value to inspect
173 * @param options
174 * @returns
175 */
176 static isAllowed$Ref(value: unknown) {
177 if (this.is$Ref(value)) {
178 if (value.$ref.substring(0, 2) === '#/' || value.$ref === '#') {
179 // It's a JSON Pointer reference, which is always allowed
180 return true;
181 } else if (value.$ref[0] !== '#') {
182 // It's an external reference, which is allowed by the options
183 return true;
184 }
185 }
186 return undefined;
187 }
188
189 /**
190 * Determines whether the given value is a JSON reference that "extends" its resolved value.
191 * That is, it has extra properties (in addition to "$ref"), so rather than simply pointing to
192 * an existing value, this $ref actually creates a NEW value that is a shallow copy of the resolved
193 * value, plus the extra properties.
194 *
195 * @example: {
196 person: {
197 properties: {
198 firstName: { type: string }
199 lastName: { type: string }
200 }
201 }
202 employee: {
203 properties: {
204 $ref: #/person/properties
205 salary: { type: number }
206 }
207 }
208 }
209 * In this example, "employee" is an extended $ref, since it extends "person" with an additional
210 * property (salary). The result is a NEW value that looks like this:
211 *
212 * {
213 * properties: {
214 * firstName: { type: string }
215 * lastName: { type: string }
216 * salary: { type: number }
217 * }
218 * }
219 *
220 * @param value - The value to inspect
221 * @returns
222 */
223 static isExtended$Ref(value: unknown) {
224 return $Ref.is$Ref(value) && Object.keys(value).length > 1;
225 }
226
227 /**
228 * Returns the resolved value of a JSON Reference.
229 * If necessary, the resolved value is merged with the JSON Reference to create a new object
230 *
231 * @example: {
232 person: {
233 properties: {
234 firstName: { type: string }
235 lastName: { type: string }
236 }
237 }
238 employee: {
239 properties: {
240 $ref: #/person/properties
241 salary: { type: number }
242 }
243 }
244 } When "person" and "employee" are merged, you end up with the following object:
245 *
246 * {
247 * properties: {
248 * firstName: { type: string }
249 * lastName: { type: string }
250 * salary: { type: number }
251 * }
252 * }
253 *
254 * @param $ref - The JSON reference object (the one with the "$ref" property)
255 * @param resolvedValue - The resolved value, which can be any type
256 * @returns - Returns the dereferenced value
257 */
258 static dereference<S extends object = JSONSchema>($ref: $Ref<S>, resolvedValue: S): S {
259 if (resolvedValue && typeof resolvedValue === 'object' && $Ref.isExtended$Ref($ref)) {
260 const merged = {};
261 for (const key of Object.keys($ref)) {
262 if (key !== '$ref') {
263 // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
264 merged[key] = $ref[key];
265 }
266 }
267
268 for (const key of Object.keys(resolvedValue)) {
269 if (!(key in merged)) {
270 // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
271 merged[key] = resolvedValue[key];
272 }
273 }
274
275 return merged as S;
276 } else {
277 // Completely replace the original reference with the resolved value
278 return resolvedValue;
279 }
280 }
281}
282
283export default $Ref;