You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
6.3 KiB
239 lines
6.3 KiB
'use strict'
|
|
|
|
const URI = require('uri-js')
|
|
const cloner = require('rfdc')({ proto: true, circles: false })
|
|
const { EventEmitter } = require('events')
|
|
const debug = require('debug')('json-schema-resolver')
|
|
|
|
const kIgnore = Symbol('json-schema-resolver.ignore') // untrack a schema (usually the root one)
|
|
const kRefToDef = Symbol('json-schema-resolver.refToDef') // assign to an external json a new reference
|
|
const kConsumed = Symbol('json-schema-resolver.consumed') // when an external json has been referenced
|
|
|
|
// ! Target: DRAFT-07
|
|
// https://tools.ietf.org/html/draft-handrews-json-schema-01
|
|
|
|
// ? Open to DRAFT 08
|
|
// https://json-schema.org/draft/2019-09/json-schema-core.html
|
|
|
|
const defaultOpts = {
|
|
target: 'draft-07',
|
|
clone: false,
|
|
buildLocalReference (json, baseUri, fragment, i) {
|
|
return `def-${i}`
|
|
}
|
|
}
|
|
|
|
const targetSupported = ['draft-07'] // TODO , 'draft-08'
|
|
const targetCfg = {
|
|
'draft-07': {
|
|
def: 'definitions'
|
|
},
|
|
'draft-08': {
|
|
def: '$defs'
|
|
}
|
|
}
|
|
|
|
// logic: https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.B.1
|
|
function jsonSchemaResolver (options) {
|
|
const ee = new EventEmitter()
|
|
const {
|
|
clone,
|
|
target,
|
|
applicationUri,
|
|
externalSchemas: rootExternalSchemas,
|
|
buildLocalReference
|
|
} = Object.assign({}, defaultOpts, options)
|
|
|
|
const allIds = new Map()
|
|
let rolling = 0
|
|
ee.on('$id', collectIds)
|
|
|
|
const allRefs = []
|
|
ee.on('$ref', collectRefs)
|
|
|
|
if (!targetSupported.includes(target)) {
|
|
throw new Error(`Unsupported JSON schema version ${target}`)
|
|
}
|
|
|
|
let defaultUri
|
|
if (applicationUri) {
|
|
defaultUri = getRootUri(applicationUri)
|
|
|
|
if (rootExternalSchemas) {
|
|
for (const es of rootExternalSchemas) { mapIds(ee, defaultUri, es) }
|
|
debug('Processed root external schemas')
|
|
}
|
|
} else if (rootExternalSchemas) {
|
|
throw new Error('If you set root externalSchema, the applicationUri option is needed')
|
|
}
|
|
|
|
return {
|
|
resolve,
|
|
definitions () {
|
|
const defKey = targetCfg[target].def
|
|
const x = { [defKey]: {} }
|
|
allIds.forEach((json, baseUri) => {
|
|
x[defKey][json[kRefToDef]] = json
|
|
})
|
|
return x
|
|
}
|
|
}
|
|
|
|
function resolve (rootSchema, opts) {
|
|
const { externalSchemas } = opts || {}
|
|
|
|
if (!rootExternalSchemas) {
|
|
allIds.clear()
|
|
}
|
|
allRefs.length = 0
|
|
|
|
if (clone) {
|
|
rootSchema = cloner(rootSchema)
|
|
}
|
|
|
|
const appUri = defaultUri || getRootUri(rootSchema.$id)
|
|
debug('Found app URI %o', appUri)
|
|
|
|
if (externalSchemas) {
|
|
for (const es of externalSchemas) { mapIds(ee, appUri, es) }
|
|
debug('Processed external schemas')
|
|
}
|
|
|
|
const baseUri = URI.serialize(appUri) // canonical absolute-URI
|
|
if (rootSchema.$id) {
|
|
rootSchema.$id = baseUri // fix the schema $id value
|
|
}
|
|
rootSchema[kIgnore] = true
|
|
|
|
mapIds(ee, appUri, rootSchema)
|
|
debug('Processed root schema')
|
|
|
|
debug('Generating %d refs', allRefs.length)
|
|
allRefs.forEach(({ baseUri, ref, refUri, json }) => {
|
|
debug('Evaluating $ref %s', ref)
|
|
if (ref[0] === '#') { return }
|
|
|
|
const evaluatedJson = allIds.get(baseUri)
|
|
if (!evaluatedJson) {
|
|
debug('External $ref %s not provided with baseUri %s', ref, baseUri)
|
|
return
|
|
}
|
|
evaluatedJson[kConsumed] = true
|
|
json.$ref = `#/definitions/${evaluatedJson[kRefToDef]}${refUri.fragment || ''}`
|
|
})
|
|
|
|
if (externalSchemas) {
|
|
// only if user sets external schema add it to the definitions
|
|
const defKey = targetCfg[target].def
|
|
allIds.forEach((json, baseUri) => {
|
|
if (json[kConsumed] === true) {
|
|
if (!rootSchema[defKey]) {
|
|
rootSchema[defKey] = {}
|
|
}
|
|
|
|
rootSchema[defKey][json[kRefToDef]] = json
|
|
}
|
|
})
|
|
}
|
|
|
|
return rootSchema
|
|
}
|
|
|
|
function collectIds (json, baseUri, fragment) {
|
|
if (json[kIgnore]) { return }
|
|
|
|
const rel = (fragment && URI.serialize(fragment)) || ''
|
|
const id = URI.serialize(baseUri) + rel
|
|
if (!allIds.has(id)) {
|
|
debug('Collected $id %s', id)
|
|
json[kRefToDef] = buildLocalReference(json, baseUri, fragment, rolling++)
|
|
allIds.set(id, json)
|
|
} else {
|
|
debug('WARN duplicated id %s .. IGNORED - ', id)
|
|
}
|
|
}
|
|
|
|
function collectRefs (json, baseUri, refVal) {
|
|
const refUri = URI.parse(refVal)
|
|
debug('Pre enqueue $ref %o', refUri)
|
|
|
|
// "same-document";
|
|
// "relative";
|
|
// "absolute";
|
|
// "uri";
|
|
if (refUri.reference === 'relative') {
|
|
refUri.scheme = baseUri.scheme
|
|
refUri.userinfo = baseUri.userinfo
|
|
refUri.host = baseUri.host
|
|
refUri.port = baseUri.port
|
|
|
|
const newBaseUri = Object.assign({}, baseUri)
|
|
newBaseUri.path = refUri.path
|
|
baseUri = newBaseUri
|
|
} else if (refUri.reference === 'uri' || refUri.reference === 'absolute') {
|
|
baseUri = { ...refUri, fragment: undefined }
|
|
}
|
|
|
|
const ref = URI.serialize(refUri)
|
|
allRefs.push({
|
|
baseUri: URI.serialize(baseUri),
|
|
refUri,
|
|
ref,
|
|
json
|
|
})
|
|
debug('Enqueue $ref %s', ref)
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {URI} baseUri
|
|
* @param {*} json
|
|
*/
|
|
function mapIds (ee, baseUri, json) {
|
|
if (!(json instanceof Object)) return
|
|
|
|
if (json.$id) {
|
|
const $idUri = URI.parse(json.$id)
|
|
let fragment = null
|
|
|
|
if ($idUri.reference === 'absolute') {
|
|
// "$id": "http://example.com/root.json"
|
|
baseUri = $idUri // a new baseURI for children
|
|
} else if ($idUri.reference === 'relative') {
|
|
// "$id": "other.json",
|
|
const newBaseUri = Object.assign({}, baseUri)
|
|
newBaseUri.path = $idUri.path
|
|
newBaseUri.fragment = $idUri.fragment
|
|
baseUri = newBaseUri
|
|
} else {
|
|
// { "$id": "#bar" }
|
|
fragment = $idUri
|
|
}
|
|
ee.emit('$id', json, baseUri, fragment)
|
|
}
|
|
// else if (json.$anchor) {
|
|
// TODO the $id should manage $anchor to support draft 08
|
|
// }
|
|
|
|
const fields = Object.keys(json)
|
|
for (const prop of fields) {
|
|
if (prop === '$ref') {
|
|
ee.emit('$ref', json, baseUri, json[prop])
|
|
}
|
|
mapIds(ee, baseUri, json[prop])
|
|
}
|
|
}
|
|
|
|
function getRootUri (strUri = 'application.uri') {
|
|
// If present, the value for this keyword MUST be a string, and MUST
|
|
// represent a valid URI-reference [RFC3986]. This value SHOULD be
|
|
// normalized, and SHOULD NOT be an empty fragment <#> or an empty
|
|
// string <>.
|
|
const uri = URI.parse(strUri)
|
|
uri.fragment = undefined
|
|
return uri
|
|
}
|
|
|
|
module.exports = jsonSchemaResolver
|