123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- /**
- * 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;
- };
|