• Jump To … +
    decodeToken.ts dereference.ts encodeToken.ts get.ts index.ts isPointer.ts set.ts
  • dereference.ts

  • ¶

    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.

    Usage

    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

    • {Error}: If something went wrong when dereferencing the schema.

    Dependencies

    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';
  • ¶

    Implementation

  • ¶

    Here begins the implementation of the dereference function. This being version 2 there are some specific goals being targeted.

    Design Goals:

    • JSON in JSON out. Any valid json value as defined by the spec will do.
    • More robust json pointer support, including circular references. Correctness is paramount.
    • Caching of schema lookups.
    • Cleaner and more modular design of codebase. It is ok to sacrifice performance for this.
    const isHttp: RegExp = /^http/;
    const isRemoteRef = (ref: string): boolean => isHttp.test(ref);
    
    export const dereference: Dereferencer = (root, resolver) => {
  • ¶

    JSON In, JSON Out

    The json specification section 2.1 states:

  • ¶

    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;
    };