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
 |