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
 |