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
			| 
								 
											3 years ago
										 
									 | 
							
								'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
							 |