import * as forIn from 'lodash.forin';
import * as isObject from 'lodash.isobject';
import * as merge from 'lodash.merge';
import { get, isPointer, set, encodeToken } from './index';
import { Dereferencer, Resolver } from './types';
The dereference
function dereferences schema, that is it resolves all
$ref
declarations in a schema and inlines those references into one logical
schema in accordance with the IETF json reference
draft-03
specification.
import { dereference } from '@jdw/jst';
const schema = {
foo: { $ref: '#/definitions/foo' },
definitions: { foo: 123 }
};
dereference(schema); // { foo: 123, definitions: { foo: 123 }};
Arguments
subject: Object|number|string|boolean|null
A json value.resolve:
Resolver` A function to resolve a schema by its id.Returns
any
: The dereferenced object.Throws
import * as forIn from 'lodash.forin';
import * as isObject from 'lodash.isobject';
import * as merge from 'lodash.merge';
import { get, isPointer, set, encodeToken } from './index';
import { Dereferencer, Resolver } from './types';
Here begins the implementation of the dereference
function. This being
version 2 there are some specific goals being targeted.
Design Goals:
const isHttp: RegExp = /^http/;
const isRemoteRef = (ref: string): boolean => isHttp.test(ref);
export const dereference: Dereferencer = (root, resolver) => {
A JSON value MUST be an object, array, number, or string, or one of the following three literal names: false null true
Any other value should result in an TypeError
being thrown.
if (!(typeof root).match(/object|string|number|boolean/)) {
throw new TypeError(
`@jst/dereference: argument not a valid json value: ${typeof root} | ${root}`);
}
const circularRefs = {};
const walk = (schema: any, resolve: Resolver = null, path: string = '#'): any => {
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((scm, index) => walk(scm, resolve, `${path}/${index}`))
.reduce((acc, scm) => merge(acc, scm), {});
If schema is not an array of json objects we expect a singlular json schema be provided
} else if (isObject(schema)) {
const schemaId = schema.id || undefined;
let isCircular = false;
traverse is an internal recursive function that we bind to this lexical scope in order to easily resolve to schema definitions whilst traversing an objects nested properties. This is primarily for efficiency concerns.
const traverse = (node, nodePath: string = '#') => {
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, index) => traverse(v, `${nodePath}/${index}`));
}
if we are here, the first argument is not an array or value and we expect it to be a json schema.
forIn(node, (value, key) => {
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;
Here we resolve a JSON reference (uri). In order to do so correctly we must make a distinction between external references and internal (circular) references.
if (isRemoteRef(value)) {
if (!resolve) {
throw new TypeError(
'argument: resolver is required to dereference a json uri.');
}
if (value !== schemaId) {
reference = resolve(value);
if (!reference) {
throw new Error(`unable to resolve URI reference: ${value}`);
}
resolution = merge(
resolution,
walk(reference, resolve, `${nodePath}/${encodeToken(key)}`),
true,
);
} else {
reference = resolution;
circularRefs[nodePath] = schema;
isCircular = true;
}
de-reference a json pointer
} else if (isPointer(value)) {
reference = get(schema, value);
resolution = merge(
resolution,
traverse(reference, `${nodePath}/${encodeToken(key)}`),
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, `${nodePath}/${encodeToken(key)}`);
}
});
return resolution;
};
return traverse(schema, path);
if any other combination of arguments is provided we throw
} else {
throw new TypeError(`expected first parameter to be object or array: ${schema}`);
}
};
const result = walk(root, resolver);
We can now handle any circular references in the schema by iterating our
store of circular references encountered whilst processing the schema. We will
only dereference a circular schema once, I could write a monologue about
this topic but let it suffice to say JST does not make the decision what is
the correct amount of circular depth to dereference, we only do so
once. Users can simply call dereference
again with the resultant schema
to get another level of nesting.
forIn(circularRefs, (value, key) => {
set(result, key.split('$ref/').join(''), value);
});
return result;
};