fork of hey-api/openapi-ts because I need some additional things
at feat/use-query-options-param 283 lines 8.5 kB view raw
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;