This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+317
View File
@@ -0,0 +1,317 @@
const path = require('path')
const _ = require('lodash')
const clone = require('clone')
const traverse = require('traverse')
const DAG = require('dag-map')
const md5 = require('md5')
const utils = require('./utils')
const fileLoader = require('./loaders/file')
const defaults = {
baseFolder: process.cwd()
}
let cache = {}
const loaders = {
file: fileLoader
}
function getLoader (refType, options) {
return _.get(options, `loaders.${refType}`, loaders[refType]);
}
/**
* Returns the reference schema that refVal points to.
* If the ref val points to a ref within a file, the file is loaded and fully derefed, before we get the
* pointing property. Derefed files are cached.
*
* @param refVal
* @param refType
* @param parent
* @param options
* @param state
* @private
*/
function getRefSchema (refVal, refType, parent, options, state) {
const loader = getLoader(refType, options)
if (refType && loader) {
let newVal
let oldBasePath
let loaderValue
let filePath
let fullRefFilePath
if (refType === 'file') {
filePath = utils.getRefFilePath(refVal)
fullRefFilePath = utils.isAbsolute(filePath) ? filePath : path.resolve(state.cwd, filePath)
if (cache[fullRefFilePath]) {
loaderValue = cache[fullRefFilePath]
}
}
if (!loaderValue) {
loaderValue = loader(refVal, options)
if (loaderValue) {
// adjust base folder if needed so that we can handle paths in nested folders
if (refType === 'file') {
let dirname = path.dirname(filePath)
if (dirname === '.') {
dirname = ''
}
if (dirname) {
oldBasePath = state.cwd
const newBasePath = path.resolve(state.cwd, dirname)
options.baseFolder = state.cwd = newBasePath
}
}
loaderValue = derefSchema(loaderValue, options, state)
// reset
if (oldBasePath) {
options.baseFolder = state.cwd = oldBasePath
}
}
}
if (loaderValue) {
if (refType === 'file' && fullRefFilePath && !cache[fullRefFilePath]) {
cache[fullRefFilePath] = loaderValue
}
if (refVal.indexOf('#') >= 0) {
const refPaths = refVal.split('#')
const refPath = refPaths[1]
const refNewVal = utils.getRefPathValue(loaderValue, refPath)
if (refNewVal) {
newVal = refNewVal
}
} else {
newVal = loaderValue
}
}
return newVal
} else if (refType === 'local') {
return utils.getRefPathValue(parent, refVal)
}
}
/**
* Add to state history
* @param {Object} state the state
* @param {String} type ref type
* @param {String} value ref value
* @private
*/
function addToHistory (state, type, value) {
let dest
if (type === 'file') {
dest = utils.getRefFilePath(value)
} else {
if (value === '#') {
return false
}
dest = state.current.concat(`:${value}`)
}
if (dest) {
dest = dest.toLowerCase()
if (state.history.indexOf(dest) >= 0) {
return false
}
state.history.push(dest)
}
return true
}
/**
* Set the current into state
* @param {Object} state the state
* @param {String} type ref type
* @param {String} value ref value
* @private
*/
function setCurrent (state, type, value) {
let dest
if (type === 'file') {
dest = utils.getRefFilePath(value)
}
if (dest) {
state.current = dest
}
}
/**
* Check the schema for local circular refs using DAG
* @param {Object} schema the schema
* @return {Error|undefined} <code>Error</code> if circular ref, <code>undefined</code> otherwise if OK
* @private
*/
function checkLocalCircular (schema) {
const dag = new DAG()
const locals = traverse(schema).reduce(function (acc, node) {
if (!_.isNull(node) && !_.isUndefined(null) && typeof node.$ref === 'string') {
const refType = utils.getRefType(node)
if (refType === 'local') {
const value = utils.getRefValue(node)
if (value) {
const path = this.path.join('/')
acc.push({
from: path,
to: value
})
}
}
}
return acc
}, [])
if (!locals || !locals.length) {
return
}
if (_.some(locals, elem => elem.to === '#')) {
return new Error('Circular self reference')
}
const check = _.find(locals, elem => {
const fromEdge = elem.from.concat('/')
const dest = elem.to.substring(2).concat('/')
try {
dag.addEdge(fromEdge, dest)
} catch (e) {
return elem
}
if (fromEdge.indexOf(dest) === 0) {
return elem
}
})
if (check) {
return new Error(`Circular self reference from ${check.from} to ${check.to}`)
}
}
/**
* Derefs $ref types in a schema
* @param schema
* @param options
* @param state
* @param type
* @private
*/
function derefSchema (schema, options, state) {
const check = checkLocalCircular(schema)
if (check instanceof Error) {
return check
}
if (state.circular) {
return new Error(`circular references found: ${state.circularRefs.toString()}`)
} else if (state.error) {
return state.error
}
return traverse(schema).forEach(function (node) {
if (!_.isNull(node) && !_.isUndefined(null) && typeof node.$ref === 'string') {
const refType = utils.getRefType(node)
const refVal = utils.getRefValue(node)
const addOk = addToHistory(state, refType, refVal)
if (!addOk) {
state.circular = true
state.circularRefs.push(refVal)
state.error = new Error(`circular references found: ${state.circularRefs.toString()}`)
this.update(node, true)
} else {
setCurrent(state, refType, refVal)
let newValue = getRefSchema(refVal, refType, schema, options, state)
state.history.pop()
if (newValue === undefined) {
if (state.missing.indexOf(refVal) === -1) {
state.missing.push(refVal)
}
if (options.failOnMissing) {
state.error = new Error(`Missing $ref: ${refVal}`)
}
this.update(node, options.failOnMissing)
} else {
if (options.removeIds && newValue.hasOwnProperty('$id')) {
delete newValue.$id
}
if (options.mergeAdditionalProperties) {
delete node.$ref
newValue = _.merge({}, newValue, node)
}
this.update(newValue)
if (state.missing.indexOf(refVal) !== -1) {
state.missing.splice(state.missing.indexOf(refVal), 1)
}
}
}
}
})
}
/**
* Derefs <code>$ref</code>'s in JSON Schema to actual resolved values. Supports local, and file refs.
* @param {Object} schema - The JSON schema
* @param {Object} options - options
* @param {String} options.baseFolder - the base folder to get relative path files from. Default is <code>process.cwd()</code>
* @param {Boolean} options.failOnMissing - By default missing / unresolved refs will be left as is with their ref value intact.
* If set to <code>true</code> we will error out on first missing ref that we cannot
* resolve. Default: <code>false</code>.
* @param {Boolean} options.mergeAdditionalProperties - By default properties in a object with $ref will be removed in the output.
* If set to <code>true</code> they will be added/overwrite the output. This will use lodash's merge function.
* Default: <code>false</code>.
* @param {Boolean} options.removeIds - By default <code>$id</code> fields will get copied when dereferencing.
* If set to <code>true</code> they will be removed. Merged properties will not get removed.
* Default: <code>false</code>.
* @param {Object} options.loaders - A hash mapping reference types (e.g., 'file') to loader functions.
* @return {Object|Error} the deref schema oran instance of <code>Error</code> if error.
*/
function deref (schema, options) {
options = _.defaults(options, defaults)
const bf = options.baseFolder
let cwd = bf
if (!utils.isAbsolute(bf)) {
cwd = path.resolve(process.cwd(), bf)
}
const state = {
graph: new DAG(),
circular: false,
circularRefs: [],
cwd: cwd,
missing: [],
history: []
}
try {
const str = JSON.stringify(schema)
state.current = md5(str)
} catch (e) {
return e
}
const baseSchema = clone(schema)
cache = {}
let ret = derefSchema(baseSchema, options, state)
if (ret instanceof Error === false && state.error) {
return state.error
}
return ret
}
module.exports = deref
+34
View File
@@ -0,0 +1,34 @@
const fs = require('fs')
const path = require('path')
const { getRefFilePath } = require('../utils')
var cwd = process.cwd()
/**
* Resolves a file link of a json schema to the actual value it references
* @param refValue the value. String. Ex. `/some/path/schema.json#/definitions/foo`
* @param options
* baseFolder - the base folder to get relative path files from. Default is `process.cwd()`
* @returns {*}
* @private
*/
module.exports = function (refValue, options) {
let refPath = refValue
const baseFolder = options.baseFolder ? path.resolve(cwd, options.baseFolder) : cwd
if (refPath.indexOf('file:') === 0) {
refPath = refPath.substring(5)
} else {
refPath = path.resolve(baseFolder, refPath)
}
const filePath = getRefFilePath(refPath)
let newValue
try {
var data = fs.readFileSync(filePath)
newValue = JSON.parse(data)
} catch (e) {}
return newValue
};
+122
View File
@@ -0,0 +1,122 @@
const path = require('path')
const validUrl = require('valid-url')
const isValidPath = require('is-valid-path')
const isWindows = process.platform === 'win32'
/**
* Gets the ref value of a search result from prop-search or ref object
* @param ref The search result object from prop-search
* @returns {*} The value of $ref or undefined if not present in search object
* @private
*/
function getRefValue (ref) {
const thing = ref ? (ref.value ? ref.value : ref) : null
if (thing && thing.$ref && typeof thing.$ref === 'string') {
return thing.$ref
}
}
exports.getRefValue = getRefValue
/**
* Gets the type of $ref from search result object.
* @param ref The search result object from prop-search or a ref object
* @returns {string} `web` if it's a web url.
* `file` if it's a file path.
* `local` if it's a link to local schema.
* undefined otherwise
* @private
*/
function getRefType (ref) {
const val = getRefValue(ref)
if (val) {
if ((val.charAt(0) === '#')) {
return 'local'
}
if (validUrl.isWebUri(val)) {
return 'web'
}
return 'file'
}
}
exports.getRefType = getRefType
/**
* Determines if object is a $ref object. That is { $ref: <something> }
* @param thing object to test
* @returns {boolean} true if passes the test. false otherwise.
* @private
*/
function isRefObject (thing) {
if (thing && typeof thing === 'object' && !Array.isArray(thing)) {
const keys = Object.keys(thing)
return keys.length === 1 && keys[0] === '$ref' && typeof thing.$ref === 'string'
}
return false
}
exports.isRefObject = isRefObject
/**
* Gets the value at the ref path within schema
* @param schema the (root) json schema to search
* @param refPath string ref path to get within the schema. Ex. `#/definitions/id`
* @returns {*} Returns the value at the path location or undefined if not found within the given schema
* @private
*/
function getRefPathValue (schema, refPath) {
let rpath = refPath
const hashIndex = refPath.indexOf('#')
if (hashIndex >= 0) {
rpath = refPath.substring(hashIndex)
if (rpath.length > 1) {
rpath = refPath.substring(1)
} else {
rpath = ''
}
}
// Walk through each /-separated path component, and get
// the value for that key (ignoring empty keys)
const keys = rpath.split('/').filter(k => !!k)
return keys.reduce(function (value, key) {
return value[key]
}, schema)
}
exports.getRefPathValue = getRefPathValue
function getRefFilePath (refPath) {
let filePath = refPath
const hashIndex = filePath.indexOf('#')
if (hashIndex > 0) {
filePath = refPath.substring(0, hashIndex)
}
return filePath
}
exports.getRefFilePath = getRefFilePath
// Regex to split a windows path into three parts: [*, device, slash,
// tail] windows-only
const splitDeviceRe = /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/
function win32StatPath (path) {
const result = splitDeviceRe.exec(path)
const device = result[1] || ''
const isUnc = !!device && device[1] !== ':'
return {
device: device,
isUnc: isUnc,
isAbsolute: isUnc || !!result[2], // UNC paths are always absolute
tail: result[3]
}
}
exports.isAbsolute = typeof path.isAbsolute === 'function' ? path.isAbsolute : function utilIsAbsolute (path) {
if (isWindows) {
return win32StatPath(path).isAbsolute
}
return !!path && path[0] === '/'
}