285 lines
8.2 KiB
JavaScript
285 lines
8.2 KiB
JavaScript
'use strict'
|
|
|
|
const { dequal: deepEqual } = require('dequal')
|
|
|
|
const jsonSchemaRefSymbol = Symbol.for('json-schema-ref')
|
|
|
|
class RefResolver {
|
|
#schemas
|
|
#derefSchemas
|
|
#insertRefSymbol
|
|
#allowEqualDuplicates
|
|
#cloneSchemaWithoutRefs
|
|
|
|
constructor (opts = {}) {
|
|
this.#schemas = {}
|
|
this.#derefSchemas = {}
|
|
this.#insertRefSymbol = opts.insertRefSymbol ?? false
|
|
this.#allowEqualDuplicates = opts.allowEqualDuplicates ?? true
|
|
this.#cloneSchemaWithoutRefs = opts.cloneSchemaWithoutRefs ?? false
|
|
}
|
|
|
|
addSchema (schema, rootSchemaId, isRootSchema = true) {
|
|
if (isRootSchema) {
|
|
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
|
|
// Schema has an $id that is not an anchor
|
|
rootSchemaId = schema.$id
|
|
} else {
|
|
// Schema has no $id or $id is an anchor
|
|
this.#insertSchemaBySchemaId(schema, rootSchemaId)
|
|
}
|
|
}
|
|
|
|
const schemaId = schema.$id
|
|
if (schemaId !== undefined && typeof schemaId === 'string') {
|
|
if (schemaId.charAt(0) === '#') {
|
|
this.#insertSchemaByAnchor(schema, rootSchemaId, schemaId)
|
|
} else {
|
|
this.#insertSchemaBySchemaId(schema, schemaId)
|
|
rootSchemaId = schemaId
|
|
}
|
|
}
|
|
|
|
const ref = schema.$ref
|
|
if (ref !== undefined && typeof ref === 'string') {
|
|
const { refSchemaId, refJsonPointer } = this.#parseSchemaRef(ref, rootSchemaId)
|
|
this.#schemas[rootSchemaId].refs.push({
|
|
schemaId: refSchemaId,
|
|
jsonPointer: refJsonPointer
|
|
})
|
|
}
|
|
|
|
for (const key in schema) {
|
|
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
this.addSchema(schema[key], rootSchemaId, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
getSchema (schemaId, jsonPointer = '#') {
|
|
const schema = this.#schemas[schemaId]
|
|
if (schema === undefined) {
|
|
throw new Error(
|
|
`Cannot resolve ref "${schemaId}${jsonPointer}". Schema with id "${schemaId}" is not found.`
|
|
)
|
|
}
|
|
if (schema.anchors[jsonPointer] !== undefined) {
|
|
return schema.anchors[jsonPointer]
|
|
}
|
|
return getDataByJSONPointer(schema.schema, jsonPointer)
|
|
}
|
|
|
|
hasSchema (schemaId) {
|
|
return this.#schemas[schemaId] !== undefined
|
|
}
|
|
|
|
getSchemaRefs (schemaId) {
|
|
const schema = this.#schemas[schemaId]
|
|
if (schema === undefined) {
|
|
throw new Error(`Schema with id "${schemaId}" is not found.`)
|
|
}
|
|
return schema.refs
|
|
}
|
|
|
|
getSchemaDependencies (schemaId, dependencies = {}) {
|
|
const schema = this.#schemas[schemaId]
|
|
|
|
for (const ref of schema.refs) {
|
|
const dependencySchemaId = ref.schemaId
|
|
if (
|
|
dependencySchemaId === schemaId ||
|
|
dependencies[dependencySchemaId] !== undefined
|
|
) continue
|
|
|
|
dependencies[dependencySchemaId] = this.getSchema(dependencySchemaId)
|
|
this.getSchemaDependencies(dependencySchemaId, dependencies)
|
|
}
|
|
|
|
return dependencies
|
|
}
|
|
|
|
derefSchema (schemaId) {
|
|
if (this.#derefSchemas[schemaId] !== undefined) return
|
|
|
|
const schema = this.#schemas[schemaId]
|
|
if (schema === undefined) {
|
|
throw new Error(`Schema with id "${schemaId}" is not found.`)
|
|
}
|
|
|
|
if (!this.#cloneSchemaWithoutRefs && schema.refs.length === 0) {
|
|
this.#derefSchemas[schemaId] = {
|
|
schema: schema.schema,
|
|
anchors: schema.anchors
|
|
}
|
|
}
|
|
|
|
const refs = []
|
|
this.#addDerefSchema(schema.schema, schemaId, true, refs)
|
|
|
|
const dependencies = this.getSchemaDependencies(schemaId)
|
|
for (const schemaId in dependencies) {
|
|
const schema = dependencies[schemaId]
|
|
this.#addDerefSchema(schema, schemaId, true, refs)
|
|
}
|
|
|
|
for (const ref of refs) {
|
|
const {
|
|
refSchemaId,
|
|
refJsonPointer
|
|
} = this.#parseSchemaRef(ref.ref, ref.sourceSchemaId)
|
|
|
|
const targetSchema = this.getDerefSchema(refSchemaId, refJsonPointer)
|
|
if (targetSchema === null) {
|
|
throw new Error(
|
|
`Cannot resolve ref "${ref.ref}". Ref "${refJsonPointer}" is not found in schema "${refSchemaId}".`
|
|
)
|
|
}
|
|
|
|
ref.targetSchema = targetSchema
|
|
ref.targetSchemaId = refSchemaId
|
|
}
|
|
|
|
for (const ref of refs) {
|
|
this.#resolveRef(ref, refs)
|
|
}
|
|
}
|
|
|
|
getDerefSchema (schemaId, jsonPointer = '#') {
|
|
let derefSchema = this.#derefSchemas[schemaId]
|
|
if (derefSchema === undefined) {
|
|
this.derefSchema(schemaId)
|
|
derefSchema = this.#derefSchemas[schemaId]
|
|
}
|
|
if (derefSchema.anchors[jsonPointer] !== undefined) {
|
|
return derefSchema.anchors[jsonPointer]
|
|
}
|
|
return getDataByJSONPointer(derefSchema.schema, jsonPointer)
|
|
}
|
|
|
|
#parseSchemaRef (ref, schemaId) {
|
|
const sharpIndex = ref.indexOf('#')
|
|
if (sharpIndex === -1) {
|
|
return { refSchemaId: ref, refJsonPointer: '#' }
|
|
}
|
|
if (sharpIndex === 0) {
|
|
return { refSchemaId: schemaId, refJsonPointer: ref }
|
|
}
|
|
return {
|
|
refSchemaId: ref.slice(0, sharpIndex),
|
|
refJsonPointer: ref.slice(sharpIndex)
|
|
}
|
|
}
|
|
|
|
#addDerefSchema (schema, rootSchemaId, isRootSchema, refs = []) {
|
|
const derefSchema = Array.isArray(schema) ? [...schema] : { ...schema }
|
|
|
|
if (isRootSchema) {
|
|
if (schema.$id !== undefined && schema.$id.charAt(0) !== '#') {
|
|
// Schema has an $id that is not an anchor
|
|
rootSchemaId = schema.$id
|
|
} else {
|
|
// Schema has no $id or $id is an anchor
|
|
this.#insertDerefSchemaBySchemaId(derefSchema, rootSchemaId)
|
|
}
|
|
}
|
|
|
|
const schemaId = derefSchema.$id
|
|
if (schemaId !== undefined && typeof schemaId === 'string') {
|
|
if (schemaId.charAt(0) === '#') {
|
|
this.#insertDerefSchemaByAnchor(derefSchema, rootSchemaId, schemaId)
|
|
} else {
|
|
this.#insertDerefSchemaBySchemaId(derefSchema, schemaId)
|
|
rootSchemaId = schemaId
|
|
}
|
|
}
|
|
|
|
if (derefSchema.$ref !== undefined) {
|
|
refs.push({
|
|
ref: derefSchema.$ref,
|
|
sourceSchemaId: rootSchemaId,
|
|
sourceSchema: derefSchema
|
|
})
|
|
}
|
|
|
|
for (const key in derefSchema) {
|
|
const value = derefSchema[key]
|
|
if (typeof value === 'object' && value !== null) {
|
|
derefSchema[key] = this.#addDerefSchema(value, rootSchemaId, false, refs)
|
|
}
|
|
}
|
|
|
|
return derefSchema
|
|
}
|
|
|
|
#resolveRef (ref, refs) {
|
|
const { sourceSchema, targetSchema } = ref
|
|
|
|
if (!sourceSchema.$ref) return
|
|
if (this.#insertRefSymbol) {
|
|
sourceSchema[jsonSchemaRefSymbol] = sourceSchema.$ref
|
|
}
|
|
|
|
delete sourceSchema.$ref
|
|
|
|
if (targetSchema.$ref) {
|
|
const targetSchemaRef = refs.find(ref => ref.sourceSchema === targetSchema)
|
|
this.#resolveRef(targetSchemaRef, refs)
|
|
}
|
|
for (const key in targetSchema) {
|
|
if (key === '$id') continue
|
|
if (sourceSchema[key] !== undefined) {
|
|
if (deepEqual(sourceSchema[key], targetSchema[key])) continue
|
|
throw new Error(
|
|
`Cannot resolve ref "${ref.ref}". Property "${key}" already exists in schema "${ref.sourceSchemaId}".`
|
|
)
|
|
}
|
|
sourceSchema[key] = targetSchema[key]
|
|
}
|
|
ref.isResolved = true
|
|
}
|
|
|
|
#insertSchemaBySchemaId (schema, schemaId) {
|
|
const foundSchema = this.#schemas[schemaId]
|
|
if (foundSchema !== undefined) {
|
|
if (this.#allowEqualDuplicates && deepEqual(schema, foundSchema.schema)) return
|
|
throw new Error(`There is already another schema with id "${schemaId}".`)
|
|
}
|
|
this.#schemas[schemaId] = { schema, anchors: {}, refs: [] }
|
|
}
|
|
|
|
#insertSchemaByAnchor (schema, schemaId, anchor) {
|
|
const { anchors } = this.#schemas[schemaId]
|
|
if (anchors[anchor] !== undefined) {
|
|
throw new Error(`There is already another anchor "${anchor}" in schema "${schemaId}".`)
|
|
}
|
|
anchors[anchor] = schema
|
|
}
|
|
|
|
#insertDerefSchemaBySchemaId (schema, schemaId) {
|
|
const foundSchema = this.#derefSchemas[schemaId]
|
|
if (foundSchema !== undefined) return
|
|
|
|
this.#derefSchemas[schemaId] = { schema, anchors: {} }
|
|
}
|
|
|
|
#insertDerefSchemaByAnchor (schema, schemaId, anchor) {
|
|
const { anchors } = this.#derefSchemas[schemaId]
|
|
anchors[anchor] = schema
|
|
}
|
|
}
|
|
|
|
function getDataByJSONPointer (data, jsonPointer) {
|
|
const parts = jsonPointer.split('/')
|
|
let current = data
|
|
for (const part of parts) {
|
|
if (part === '' || part === '#') continue
|
|
if (typeof current !== 'object' || current === null) {
|
|
return null
|
|
}
|
|
current = current[part]
|
|
}
|
|
return current ?? null
|
|
}
|
|
|
|
module.exports = { RefResolver }
|