rttc
Version:
Runtime type-checking for JavaScript.
363 lines (322 loc) • 13.2 kB
JavaScript
/**
* Module dependencies
*/
var _ = require('@sailshq/lodash');
var buildTwoHeadedSchemaCursor = require('./helpers/build-two-headed-schema-cursor');
var TYPES = require('./helpers/types');
var infer = require('./infer');
var getDefaultExemplar = require('./get-default-exemplar');
/**
* union()
*
* Given two rttc schemas, return the most specific schema that
* would accept the superset of what both schemas accept normally.
*
*
* @param {*} schema0
* @param {*} schema1
* @param {boolean} isExemplar - if set, the schemas will be treated as exemplars (rather than type schemas)
* @param {boolean} isStrict - if set, the schemas will be unioned using strict validation rules.
* @return {*}
*/
module.exports = function union (schema0, schema1, isExemplar, isStrict) {
/*
// Type union: (using strict validation rules)
(plan is to not worry about supporting uncertainty at the moment)
// Special cases:
// inside a generic dictionary keypath: act like 'json'
// inside a generic array keypath: act like 'json'
// inside a JSON keypath: act like 'json'
// inside a ref keypath: act like 'ref'
// inside any other keypath: not possible, that's an error (will be caught during stabilization, so we can ignore)
// Types always union with themselves, with an identity result.
'string' ∪ 'string' <====> 'string'
'number' ∪ 'number' <====> 'number'
'boolean' ∪ 'boolean' <====> 'boolean'
'lamda' ∪ 'lamda' <====> 'lamda'
{} ∪ {} <====> {}
[] ∪ [] <====> []
'json' ∪ 'json' <====> 'json'
'ref' ∪ 'ref' <====> 'ref'
// Every type unions with "ref", resulting in "ref"
(anything) ∪ 'ref' <====> 'ref'
// Every type but "lamda" unions with "lamda", resulting in "ref"
'lamda' ∪ (anything else) <====> 'ref'
// Every type except "ref" and "lamda" unions with "json", resulting in "json"
(anything else) ∪ 'json' <====> 'json'
// Primitive types union with most things to result in "json"
'string' ∪ 'number' <====> 'json'
'string' ∪ 'boolean' <====> 'json'
'string' ∪ 'lamda' <====> 'json'
'string' ∪ (any dictionary) <====> 'json'
'string' ∪ (any array) <====> 'json'
'number' ∪ 'string' <====> 'json'
'number' ∪ 'boolean' <====> 'json'
'number' ∪ 'lamda' <====> 'json'
'number' ∪ (any dictionary) <====> 'json'
'number' ∪ (any array) <====> 'json'
'boolean' ∪ 'number' <====> 'json'
'boolean' ∪ 'string' <====> 'json'
'boolean' ∪ 'lamda' <====> 'json'
'boolean' ∪ (any dictionary) <====> 'json'
'boolean' ∪ (any array) <====> 'json'
// Faceted dictionaries union w/ generic dictionaries to result in generic dictionaries.
{'a': 'boolean'} ∪ {} <====> {}
// Faceted dictionaries union w/ each other, recursively unioning their child properties.
// If a key is missing, the result will be a generic dictionary.
{'a': 'boolean'} ∪ {'a':'string'} <====> {'a': 'json'}
{'a': 'lamda'} ∪ {'a':'string'} <====> {'a': 'ref'}
{'a': 'boolean'} ∪ {'b':'string'} <====> {}
// Patterned arrays union w/ generic arrays to result in generic arrays.
[{'a': 'boolean'}] ∪ [] <====> []
// Patterned arrays union w/ each other, recursively unioning their patterns.
['string'] ∪ ['number'] <====> ['json']
['lamda'] ∪ ['boolean'] <====> ['ref']
[[]] ∪ ['number'] <====> ['json']
[[[]]] ∪ ['number'] <====> ['json']
[[[]]] ∪ [['number']] <====> [['json']]
[{a:'boolean'}] ∪ [{a:'string'}] <====> [{'a': 'json'}]
[{a:'boolean'}] ∪ [{b:'string'}] <====> [{}]
[{a:'boolean'}] ∪ [[{b:'string'}]] <====> ['json']
// Exceptions when NOT using strict validation:
'number' ∪ 'string' <====> 'string'
'boolean' ∪ 'string' <====> 'string'
'number' ∪ 'boolean' <====> 'number'
*/
// exemplar-vs-type-schema-agnostic type check helper
function thisSchema(schema){
return {
is: function (){
var acceptableTypes = Array.prototype.slice.call(arguments);
if (!isExemplar) {
return _.contains(acceptableTypes, schema);
}
return _.any(acceptableTypes, function (typeName){
return TYPES[typeName].isExemplar(schema);
});
},
containsType: function (){
var searchingForTypes = Array.prototype.slice.call(arguments);
return _.any(searchingForTypes, function (typeName){
if (!_.isObject(schema)) {
return false;
}
if (_.isArray(schema)) {
if (schema.length > 0) {
return false;
}
if (!_.isObject(schema[0])) {
return thisSchema(schema[0]).is(typeName);
}
return thisSchema(schema[0]).containsType(typeName);
}
return _.reduce(schema, function (memo, value, key) {
if (!_.isObject(value)) {
return memo || thisSchema(value).is(typeName);
}
return memo || thisSchema(value).containsType(typeName);
}, false);
});
}
};
}
// exemplar-vs-type-schema-agnostic helper for building return values
function normalizeResult(type){
if (!isExemplar) {
return type;
}
return getDefaultExemplar(type);
}
// Configure two-headed type schema cursor and use it to recursively
// determine the type schema union.
var twoHeadedCursor = buildTwoHeadedSchemaCursor(
// If we pass in `false` as the first argument, it indicates we're traversing
// type schemas rather than exemplars. If `true`, then it's the other way around.
!!isExemplar,
function onFacetDict(schema0, schema1, parentKeyOrIndex, iterateRecursive){
if ( thisSchema(schema1).is('ref', 'lamda') ) {
return normalizeResult('ref');
}
if (_.isArray(schema1) || !_.isObject(schema1)) {
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
return normalizeResult('json');
}
var sharedKeys = _.intersection(_.keys(schema0), _.keys(schema1));
// If there are any keys that don't exist in BOTH schemas, we'll just return
// a generic type (ref or {}) as the union. This way coercing a value to the
// unioned schema will never result in data loss (i.e. stripped keys).
var xorKeys = _.difference(_.union(_.keys(schema0), _.keys(schema1)), sharedKeys);
if (xorKeys.length > 0) {
// If either schema is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda') || thisSchema(schema0).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
return {};
}
return _.reduce(sharedKeys, function (memo, key) {
memo[key] = iterateRecursive(key);
return memo;
}, {});
},
function onPatternArray(schema0, schema1, parentKeyOrIndex, iterateRecursive){
if ( thisSchema(schema1).is('ref', 'lamda') ) {
return normalizeResult('ref');
}
if (!_.isArray(schema1)) {
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
return normalizeResult('json');
}
if (_.isEqual(schema1, [])) {
return [];
}
return [ iterateRecursive(0) ];
},
function onGenericDict(schema0, schema1, parentKeyOrIndex){
if ( thisSchema(schema1).is('ref', 'lamda') ) {
return normalizeResult('ref');
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
if (!_.isArray(schema1) && _.isObject(schema1)) {
return {};
}
return normalizeResult('json');
},
function onGenericArray(schema0, schema1, parentKeyOrIndex){
if ( thisSchema(schema1).is('ref', 'lamda') ) {
return normalizeResult('ref');
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
if (_.isArray(schema1)) {
return [];
}
return normalizeResult('json');
},
function onJson(schema0, schema1, parentKeyOrIndex) {
if ( thisSchema(schema1).is('ref', 'lamda') ) {
return normalizeResult('ref');
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
return normalizeResult('json');
},
function onRef(schema0, schema1, parentKeyOrIndex) {
return normalizeResult('ref');
},
function onLamda(schema0, schema1, parentKeyOrIndex) {
if ( thisSchema(schema1).is('lamda') ) {
return normalizeResult('lamda');
}
return normalizeResult('ref');
},
function onString(schema0, schema1, parentKeyOrIndex) {
if ( thisSchema(schema1).is('string') ) {
return schema1;
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
if (!isStrict){
if ( thisSchema(schema1).is('number', 'boolean') ) {
return schema0;
}
}
if (
thisSchema(schema1).is('number', 'boolean', 'json') ||
_.isArray(schema1) ||
_.isObject(schema1)
) {
return normalizeResult('json');
}
return normalizeResult('ref');
},
function onNumber(schema0, schema1, parentKeyOrIndex) {
if ( thisSchema(schema1).is('number') ) {
return schema1;
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
if (!isStrict){
if ( thisSchema(schema1).is('string') ) {
return schema1;
}
if ( thisSchema(schema1).is('boolean') ) {
return schema0;
}
}
if (
thisSchema(schema1).is('string', 'boolean', 'json') ||
_.isArray(schema1) ||
_.isObject(schema1)
) {
return normalizeResult('json');
}
return normalizeResult('ref');
},
function onBoolean(schema0, schema1, parentKeyOrIndex) {
if ( thisSchema(schema1).is('boolean') ) {
return schema1;
}
// If `schema1` is a faceted dictionary or patterned array which contains
// sub-values of type:` ref` (===) or `lamda` (->), then we must make the
// result a `ref` (===).
if (thisSchema(schema1).containsType('ref', 'lamda')) {
return normalizeResult('ref');
}
if (!isStrict){
if ( thisSchema(schema1).is('string', 'number') ) {
return schema1;
}
}
if (
thisSchema(schema1).is('number', 'string', 'json') ||
_.isArray(schema1) ||
_.isObject(schema1)
) {
return normalizeResult('json');
}
return normalizeResult('ref');
}
);
// Run the iterator to get the schema union.
var result = twoHeadedCursor(schema0, schema1);
// This makes sure the resulting exemplar won't be `undefined`.
if (isExemplar) {
if (_.isUndefined(result)) {
return TYPES.ref.getExemplar();
}
}
return result;
};