src/dereference.js
import { map, merge, iterate, clone, contains } from './index';
import uri from 'valid-url';
/**
* This function _dereferences_ a schema set into one logical schema in
* accordance with the IETF JSON Reference Draft v3 specification:
*
* https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
*
* It allows for one to inject a schema resolution function in order to provide
* it with referenced schema. This allows developers to be flexible in choosing
* there schema sources such as HTTP or in-memory resolvers.
*
* The dereference function also resolves JSON Pointers according to the IETF
* RFC6901 specification:
*
* https://tools.ietf.org/html/rfc6901
*
* It does so internaly rather than through an external resolver in order to
* remain efficient.
*
* @param {Array|Object} schema A schema or array of schema to dereference.
* @param {Object} resolve A function to resolve referenced schema by its id.
* @return {Object} The dereferenced schema as an object.
*/
const dereference = (schema, resolve = null) => {
// If schema is an array we dereference each schema and then merge them from
// right-to-left.
if (Array.isArray(schema)) {
// first validate our arguments assumption!
schema.forEach((s) => {
if (typeof s !== 'object' && !Array.isArray(s))
throw new TypeError(`expect typeof object got: ${typeof s}`);
});
// then dereference each schema in the array before eventually merging them
// from right to left using a reducer function.
return schema
.map((s) => dereference(s, resolve))
.reduce((accumulator, value) => merge(accumulator, value, true), {});
}
// If schema is not an array of json objects we expect a singlular json schema
// be provided
else if (typeof schema === 'object' && !Array.isArray(schema)) {
// traverse is an internal recursive function that we bind to this lexical
// scope in order to easily resolve to schema definitons whilst traversing
// an objects nested properties. This is primarily for efficiency concerns.
const traverse = (node) => {
let resolution = {};
if (typeof node !== 'object' || node === null) return node;
// if only one argument is provided and it is an array we must recursively
// dereference it's individual values
if (Array.isArray(node)) {
return node.map((v) => traverse(v));
}
// if we are here, the first argument is not an array or value and we expect
// it to be a json schema.
iterate(node, (key, value) => {
// Skip the following properties
if (key === 'definitions') return;
// If value is not an array, object, or JSON schema reference we can
// dereference it immediately. 'typeof array' equals 'object' in JS.
if (typeof value !== 'object' && key !== '$ref') {
resolution[key] = value;
}
// If we have a schema reference we must fetch it, dereference it, then merge
// it into the base schema object.
else if (key === '$ref') {
// We have two types of references - definitions which are defined
// within the current schema and external schema references which we
// have to query AJV for as such we must fetch the schema for the
// reference appropriately.
let reference = null;
// de-reference a json uri
if (uri.isUri(value)) {
if (!resolve)
throw new TypeError(
'resolver function is required to dereference a json uri.')
;
reference = resolve(value);
if (!reference) throw new Error(`unable to resolve URI reference: ${value}`);
resolution = merge(
resolution,
dereference(reference, resolve),
true
);
}
// de-reference a json pointer
else if (
value.indexOf('#') === 0 ||
value.indexOf('/') === 0 ||
value === ''
) {
const fragments = value.split('/');
reference = fragments.reduce((acc, token) => {
// when root document pointer return accumulator
if (token === '#' || token === '/' || token === '')
return acc;
// decode token according to spec
const refToken = token.replace('~1', '/').replace('~0', '~');
let refValue = null;
// if current accumulator is array we must dereference the array
// index
if (Array.isArray(acc)) {
const index = parseInt(token, 10);
if (!acc.indexOf(index))
throw new Error(`could not dereference array index ${value}`);
refValue = acc[index];
}
// otherwise we expect an object, validate reference token
else {
if (!contains(acc, refToken))
throw new Error(`could not dereference pointer ${value}`);
refValue = acc[refToken];
}
return refValue;
}, schema);
resolution = merge(
resolution,
traverse(reference),
true
);
}
else {
throw new Error(
`could not dereference value as a json pointer or uri: ${value}`);
}
if (!reference)
throw new ReferenceError(`could not find a reference to ${value}`);
}
// Otherwise the value is an array or object and we need to traverse it
// and dereference it's properties.
else {
resolution[key] = traverse(value);
}
});
return resolution;
};
return traverse(schema);
}
// if any other combination of arguments is provided we throw
else {
throw new TypeError(`expected first parameter to be object or array: ${schema}`);
}
};
export default dereference;