diff --git a/lib/types/alternatives.js b/lib/types/alternatives.js index 875f2b090..aca726b83 100755 --- a/lib/types/alternatives.js +++ b/lib/types/alternatives.js @@ -1,7 +1,8 @@ 'use strict'; const Assert = require('@hapi/hoek/lib/assert'); -const Merge = require('@hapi/hoek/lib/merge'); +const Clone = require('@hapi/hoek/lib/clone'); +const Utils = require('@hapi/hoek/lib/utils'); const Any = require('./any'); const Common = require('../common'); @@ -12,6 +13,80 @@ const Ref = require('../ref'); const internals = {}; +// note: placed here purely for illustration purposes. will move to appropriate location before merging +// merges source properties into target, but only when they differ from the value in original +const differenceMerge = function (target, source, original, options) { + + Assert(target && typeof target === 'object', 'Invalid target value: must be an object'); + Assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object'); + Assert(original && typeof original === 'object', 'Invalid original value: must be an object'); + + if (!source) { + return target; + } + + options = Object.assign({ nullOverride: true, mergeArrays: true }, options); + + if (Array.isArray(source)) { + Assert(Array.isArray(target), 'Cannot merge array onto an object'); + if (!options.mergeArrays) { + target.length = 0; // Must not change target assignment + } + + for (let i = 0; i < source.length; ++i) { + target.push(Clone(source[i], { symbols: options.symbols })); + } + + return target; + } + + const keys = Utils.keys(source, options); + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + if (key === '__proto__' || + !Object.prototype.propertyIsEnumerable.call(source, key)) { + + continue; + } + + const value = source[key]; + if (value && + typeof value === 'object') { + + if (target[key] === value) { + continue; // Can occur for shallow merges + } + + if (!target[key] || + typeof target[key] !== 'object' || + (Array.isArray(target[key]) !== Array.isArray(value)) || + value instanceof Date || + (Buffer && Buffer.isBuffer(value)) || // $lab:coverage:ignore$ + value instanceof RegExp) { + if (value !== original[key]) { + target[key] = Clone(value, { symbols: options.symbols }); + } + } + else { + differenceMerge(target[key], value, original[key], options); + } + } + else { + if (value !== original[key]) { + if (value !== null && + value !== undefined) { // Explicit to preserve empty strings + target[key] = value; + } + else if (options.nullOverride) { + target[key] = value; + } + } + } + } + + return target; + +}; module.exports = Any.extend({ @@ -45,6 +120,7 @@ module.exports = Any.extend({ // Match all or one if (schema._flags.match) { + const valueClone = Clone(value); const matched = []; for (let i = 0; i < schema.$_terms.matches.length; ++i) { @@ -74,7 +150,14 @@ module.exports = Any.extend({ } const allobj = schema.$_terms.matches.reduce((acc, v) => acc && v.schema.type === 'object', true); - return allobj ? { value: matched.reduce((acc, v) => Merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] }; + + if (allobj) { + // add original value to set prior to merging any changed properties from matched subschemas + matched.unshift(valueClone); + return { value: matched.reduce((acc, v) => differenceMerge(acc, v, value, { mergeArrays: false })) }; + } + + return { value: matched[matched.length - 1] }; } // Match any @@ -138,7 +221,7 @@ module.exports = Any.extend({ const conditions = match.is ? [match] : match.switch; for (const item of conditions) { if (item.then && - item.otherwise) { + item.otherwise) { obj.$_setFlag('_endedSwitch', true, { clone: false }); break; @@ -200,7 +283,7 @@ module.exports = Any.extend({ const each = (item) => { if (Common.isSchema(item) && - item.type === 'array') { + item.type === 'array') { schema.$_setFlag('_arrayItems', true, { clone: false }); }