strong-tie/inbound-calls
0
1'use strict'2 3const { dequal: deepEqual } = require('dequal')4 5const jsonSchemaRefSymbol = Symbol.for('json-schema-ref')6 7class RefResolver {8 #schemas9 #derefSchemas10 #insertRefSymbol11 #allowEqualDuplicates12 #cloneSchemaWithoutRefs13 14 constructor (opts = {}) {15 this.#schemas = {}16 this.#derefSchemas = {}17 this.#insertRefSymbol = opts.insertRefSymbol ?? false18 this.#allowEqualDuplicates = opts.allowEqualDuplicates ?? true19 this.#cloneSchemaWithoutRefs = opts.cloneSchemaWithoutRefs ?? false20 }21 22 addSchema (schema, rootSchemaId, isRootSchema = true) {23 if (isRootSchema) {24 if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {25 // Schema has an $id that is not an anchor26 rootSchemaId = schema.$id27 } else {28 // Schema has no $id or $id is an anchor29 this.#insertSchemaBySchemaId(schema, rootSchemaId)30 }31 }32 33 const schemaId = schema.$id34 if (schemaId !== undefined && typeof schemaId === 'string') {35 if (schemaId.charAt(0) === '#') {36 this.#insertSchemaByAnchor(schema, rootSchemaId, schemaId)37 } else {38 this.#insertSchemaBySchemaId(schema, schemaId)39 rootSchemaId = schemaId40 }41 }42 43 const ref = schema.$ref44 if (ref !== undefined && typeof ref === 'string') {45 const { refSchemaId, refJsonPointer } = this.#parseSchemaRef(ref, rootSchemaId)46 this.#schemas[rootSchemaId].refs.push({47 schemaId: refSchemaId,48 jsonPointer: refJsonPointer49 })50 }51 52 for (const key in schema) {53 if (typeof schema[key] === 'object' && schema[key] !== null) {54 this.addSchema(schema[key], rootSchemaId, false)55 }56 }57 }58 59 getSchema (schemaId, jsonPointer = '#') {60 const schema = this.#schemas[schemaId]61 if (schema === undefined) {62 throw new Error(63 `Cannot resolve ref "${schemaId}${jsonPointer}". Schema with id "${schemaId}" is not found.`64 )65 }66 if (schema.anchors[jsonPointer] !== undefined) {67 return schema.anchors[jsonPointer]68 }69 return getDataByJSONPointer(schema.schema, jsonPointer)70 }71 72 hasSchema (schemaId) {73 return this.#schemas[schemaId] !== undefined74 }75 76 getSchemaRefs (schemaId) {77 const schema = this.#schemas[schemaId]78 if (schema === undefined) {79 throw new Error(`Schema with id "${schemaId}" is not found.`)80 }81 return schema.refs82 }83 84 getSchemaDependencies (schemaId, dependencies = {}) {85 const schema = this.#schemas[schemaId]86 87 for (const ref of schema.refs) {88 const dependencySchemaId = ref.schemaId89 if (90 dependencySchemaId === schemaId ||91 dependencies[dependencySchemaId] !== undefined92 ) continue93 94 dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)95 this.getSchemaDependencies(dependencySchemaId, dependencies)96 }97 98 return dependencies99 }100 101 derefSchema (schemaId) {102 if (this.#derefSchemas[schemaId] !== undefined) return103 104 const schema = this.#schemas[schemaId]105 if (schema === undefined) {106 throw new Error(`Schema with id "${schemaId}" is not found.`)107 }108 109 if (!this.#cloneSchemaWithoutRefs && schema.refs.length === 0) {110 this.#derefSchemas[schemaId] = {111 schema: schema.schema,112 anchors: schema.anchors113 }114 }115 116 const refs = []117 this.#addDerefSchema(schema.schema, schemaId, true, refs)118 119 const dependencies = this.getSchemaDependencies(schemaId)120 for (const schemaId in dependencies) {121 const schema = dependencies[schemaId]122 this.#addDerefSchema(schema, schemaId, true, refs)123 }124 125 for (const ref of refs) {126 const {127 refSchemaId,128 refJsonPointer129 } = this.#parseSchemaRef(ref.ref, ref.sourceSchemaId)130 131 const targetSchema = this.getDerefSchema(refSchemaId, refJsonPointer)132 if (targetSchema === null) {133 throw new Error(134 `Cannot resolve ref "${ref.ref}". Ref "${refJsonPointer}" is not found in schema "${refSchemaId}".`135 )136 }137 138 ref.targetSchema = targetSchema139 ref.targetSchemaId = refSchemaId140 }141 142 for (const ref of refs) {143 this.#resolveRef(ref, refs)144 }145 }146 147 getDerefSchema (schemaId, jsonPointer = '#') {148 let derefSchema = this.#derefSchemas[schemaId]149 if (derefSchema === undefined) {150 this.derefSchema(schemaId)151 derefSchema = this.#derefSchemas[schemaId]152 }153 if (derefSchema.anchors[jsonPointer] !== undefined) {154 return derefSchema.anchors[jsonPointer]155 }156 return getDataByJSONPointer(derefSchema.schema, jsonPointer)157 }158 159 #parseSchemaRef (ref, schemaId) {160 const sharpIndex = ref.indexOf('#')161 if (sharpIndex === -1) {162 return { refSchemaId: ref, refJsonPointer: '#' }163 }164 if (sharpIndex === 0) {165 return { refSchemaId: schemaId, refJsonPointer: ref }166 }167 return {168 refSchemaId: ref.slice(0, sharpIndex),169 refJsonPointer: ref.slice(sharpIndex)170 }171 }172 173 #addDerefSchema (schema, rootSchemaId, isRootSchema, refs = []) {174 const derefSchema = Array.isArray(schema) ? [...schema] : { ...schema }175 176 if (isRootSchema) {177 if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {178 // Schema has an $id that is not an anchor179 rootSchemaId = schema.$id180 } else {181 // Schema has no $id or $id is an anchor182 this.#insertDerefSchemaBySchemaId(derefSchema, rootSchemaId)183 }184 }185 186 const schemaId = derefSchema.$id187 if (schemaId !== undefined && typeof schemaId === 'string') {188 if (schemaId.charAt(0) === '#') {189 this.#insertDerefSchemaByAnchor(derefSchema, rootSchemaId, schemaId)190 } else {191 this.#insertDerefSchemaBySchemaId(derefSchema, schemaId)192 rootSchemaId = schemaId193 }194 }195 196 if (derefSchema.$ref !== undefined) {197 refs.push({198 ref: derefSchema.$ref,199 sourceSchemaId: rootSchemaId,200 sourceSchema: derefSchema201 })202 }203 204 for (const key in derefSchema) {205 const value = derefSchema[key]206 if (typeof value === 'object' && value !== null) {207 derefSchema[key] = this.#addDerefSchema(value, rootSchemaId, false, refs)208 }209 }210 211 return derefSchema212 }213 214 #resolveRef (ref, refs) {215 const { sourceSchema, targetSchema } = ref216 217 if (!sourceSchema.$ref) return218 if (this.#insertRefSymbol) {219 sourceSchema[jsonSchemaRefSymbol] = sourceSchema.$ref220 }221 222 delete sourceSchema.$ref223 224 if (targetSchema.$ref) {225 const targetSchemaRef = refs.find(ref => ref.sourceSchema === targetSchema)226 this.#resolveRef(targetSchemaRef, refs)227 }228 for (const key in targetSchema) {229 if (key === '$id') continue230 if (sourceSchema[key] !== undefined) {231 if (deepEqual(sourceSchema[key], targetSchema[key])) continue232 throw new Error(233 `Cannot resolve ref "${ref.ref}". Property "${key}" already exists in schema "${ref.sourceSchemaId}".`234 )235 }236 sourceSchema[key] = targetSchema[key]237 }238 ref.isResolved = true239 }240 241 #insertSchemaBySchemaId (schema, schemaId) {242 const foundSchema = this.#schemas[schemaId]243 if (foundSchema !== undefined) {244 if (this.#allowEqualDuplicates && deepEqual(schema, foundSchema.schema)) return245 throw new Error(`There is already another schema with id "${schemaId}".`)246 }247 this.#schemas[schemaId] = { schema, anchors: {}, refs: [] }248 }249 250 #insertSchemaByAnchor (schema, schemaId, anchor) {251 const { anchors } = this.#schemas[schemaId]252 if (anchors[anchor] !== undefined) {253 throw new Error(`There is already another anchor "${anchor}" in schema "${schemaId}".`)254 }255 anchors[anchor] = schema256 }257 258 #insertDerefSchemaBySchemaId (schema, schemaId) {259 const foundSchema = this.#derefSchemas[schemaId]260 if (foundSchema !== undefined) return261 262 this.#derefSchemas[schemaId] = { schema, anchors: {} }263 }264 265 #insertDerefSchemaByAnchor (schema, schemaId, anchor) {266 const { anchors } = this.#derefSchemas[schemaId]267 anchors[anchor] = schema268 }269}270 271function getDataByJSONPointer (data, jsonPointer) {272 const parts = jsonPointer.split('/')273 let current = data274 for (const part of parts) {275 if (part === '' || part === '#') continue276 if (typeof current !== 'object' || current === null) {277 return null278 }279 current = current[part]280 }281 return current ?? null282}283 284module.exports = { RefResolver }285 