diff --git a/lib/base.js b/lib/base.js index 8797ce11e..5eccc38ff 100755 --- a/lib/base.js +++ b/lib/base.js @@ -6,6 +6,7 @@ const Cache = require('./cache'); const Cast = require('./cast'); const Common = require('./common'); const Errors = require('./errors'); +const Extend = require('./extend'); const Manifest = require('./manifest'); const Messages = require('./messages'); const Modify = require('./modify'); @@ -22,6 +23,7 @@ internals.Base = class { constructor(type) { this._type = type; + this._definition = {}; this._ids = new Modify.Ids(); this._preferences = null; this._refs = new Ref.Manager(); @@ -104,7 +106,7 @@ internals.Base = class { cast(to) { - Hoek.assert(to === false || this._casts[to], 'Type', this._type, 'does not support casting to', to); + Hoek.assert(to === false || this._definition.cast[to], 'Type', this._type, 'does not support casting to', to); return this.setFlag('cast', to === false ? undefined : to); } @@ -477,11 +479,7 @@ internals.Base = class { obj._inners[key] = obj._inners[key].concat(inners); } - if (typeof obj._rebuild === 'function') { - obj._rebuild(); - } - - return obj; + return obj.rebuild(); } createError(code, value, local, state, prefs, options = {}) { @@ -600,127 +598,7 @@ internals.Base = class { extend(options) { - const base = Object.getPrototypeOf(this); - const prototype = Hoek.clone(base); - const schema = this._assign(Object.create(prototype)); - const def = Object.assign({}, options); // Shallow cloned - - prototype._definition = def; - - const parent = base._definition || {}; - def.messages = Messages.merge(parent.messages, def.messages); - def.properties = Object.assign({}, parent.properties, def.properties); - - // Initialize - - schema._type = def.type; - schema._ids._reachable = def.properties.reachable; - - if (def.initialize) { - def.initialize.call(schema); - } - - if (!def.args) { - def.args = parent.args; - } - - // Validate - - def.validate = internals.validate(def.validate, parent.validate); - - // Coerce - - if (def.coerce) { - if (typeof def.coerce === 'function') { - def.coerce = { method: def.coerce }; - } - - if (def.coerce.from && - !Array.isArray(def.coerce.from)) { - - def.coerce = { method: def.coerce.method, from: [].concat(def.coerce.from) }; - } - } - - def.coerce = internals.coerce(def.coerce, parent.coerce); - - // Rules - - const rules = Object.assign({}, parent.rules); - if (def.rules) { - for (const name in def.rules) { - const rule = def.rules[name]; - Hoek.assert(typeof rule === 'object', 'Invalid rule definition for', def.type, name); - - let method = rule.method; - if (method === undefined) { - method = function () { - - return this.addRule(name); - }; - } - - if (method) { - Hoek.assert(!prototype[name], 'Rule conflict in', def.type, name); - prototype[name] = method; - } - - Hoek.assert(!rules[name], 'Rule conflict in', def.type, name); - rules[name] = rule; - - if (rule.alias) { - const aliases = [].concat(rule.alias); - for (const alias of aliases) { - prototype[alias] = rule.method; - } - } - - if (rule.args) { - rule.argsByName = new Map(); - rule.args = rule.args.map((arg) => { - - if (typeof arg === 'string') { - arg = { name: arg }; - } - - Hoek.assert(!rule.argsByName.has(arg.name), 'Duplicated argument name', arg.name); - - rule.argsByName.set(arg.name, arg); - return arg; - }); - } - } - } - - def.rules = rules; - - if (def.overrides) { - prototype.super = base; - Object.assign(prototype, def.overrides); - } - - prototype._casts = Object.assign({}, prototype._casts); - - if (def.cast) { - Object.assign(prototype._casts, def.cast.to); - if (def.cast.from) { - prototype._casts[Common.symbols.castFrom] = def.cast.from; - } - } - - if (def.build) { - prototype._build = def.build; - } - - if (def.modify) { - prototype._override = def.modify; - } - - if (def.rebuild) { - prototype._rebuild = def.rebuild; - } - - return schema; + return Extend.type(this, options); } setFlag(name, value, options = {}) { @@ -856,6 +734,20 @@ internals.Base = class { return rules; } + rebuild() { + + if (!this._definition.rebuild) { + return this; + } + + Hoek.assert(!this._inRuleset(), 'Cannot add this rule inside a ruleset'); + + this._resetRegistrations(); + this._definition.rebuild(this); + this._ruleset = false; + return this; + } + // Internals _assign(target) { @@ -1014,77 +906,4 @@ internals.Base.prototype.options = internals.Base.prototype.prefs; internals.Base.prototype.preferences = internals.Base.prototype.prefs; -// Helpers - -internals.coerce = function (child, parent) { - - if (!child || - !parent) { - - return child || parent; - } - - return { - from: child.from && parent.from ? [...new Set([...child.from, ...parent.from])] : null, - method: function (schema, value, helpers) { - - let coerced; - if (!parent.from || - parent.from.includes(typeof value)) { - - coerced = parent.method(schema, value, helpers); - if (coerced) { - if (coerced.errors) { - return coerced; - } - - value = coerced.value; - } - } - - if (value !== undefined && - (!child.from || child.from.includes(typeof value))) { - - const own = child.method(schema, value, helpers); - if (own) { - if (own.errors) { - return own; - } - - coerced = own; - } - } - - return coerced; - } - }; -}; - - -internals.validate = function (child, parent) { - - if (!child || - !parent) { - - return child || parent; - } - - return function (schema, value, helpers) { - - const result = parent(schema, value, helpers); - if (result) { - if (result.errors && - (!Array.isArray(result.errors) || result.errors.length)) { - - return result; - } - - value = result.value; - } - - return child(schema, value, helpers); - }; -}; - - module.exports = new internals.Base(); diff --git a/lib/common.js b/lib/common.js index d7e4acc25..36ef15164 100755 --- a/lib/common.js +++ b/lib/common.js @@ -44,7 +44,6 @@ exports.defaults = { exports.symbols = { any: Marker('joi-any-base'), // Used to internally identify any-based types (shared with other joi versions) arraySingle: Symbol('arraySingle'), - castFrom: Symbol('castFrom'), deepDefault: Symbol('deepDefault'), literal: Symbol('literal'), prefs: Symbol('prefs'), diff --git a/lib/extend.js b/lib/extend.js new file mode 100755 index 000000000..1614ec86e --- /dev/null +++ b/lib/extend.js @@ -0,0 +1,253 @@ +'use strict'; + +const Hoek = require('@hapi/hoek'); + +const Messages = require('./messages'); + + +const internals = {}; + + +exports.type = function (from, options) { + + const base = Object.getPrototypeOf(from); + const prototype = Hoek.clone(base); + const schema = from._assign(Object.create(prototype)); + const def = Object.assign({}, options); // Shallow cloned + + prototype._definition = def; + + const parent = base._definition || {}; + def.messages = Messages.merge(parent.messages, def.messages); + def.properties = Object.assign({}, parent.properties, def.properties); + + // Initialize + + schema._type = def.type; + + if (def.initialize) { + def.initialize.call(schema); + } + + if (!def.args) { + def.args = parent.args; + } + + // Validate + + def.validate = internals.validate(def.validate, parent.validate); + + // Coerce + + if (def.coerce) { + if (typeof def.coerce === 'function') { + def.coerce = { method: def.coerce }; + } + + if (def.coerce.from && + !Array.isArray(def.coerce.from)) { + + def.coerce = { method: def.coerce.method, from: [].concat(def.coerce.from) }; + } + } + + def.coerce = internals.coerce(def.coerce, parent.coerce); + + // Rules + + const rules = Object.assign({}, parent.rules); + if (def.rules) { + for (const name in def.rules) { + const rule = def.rules[name]; + Hoek.assert(typeof rule === 'object', 'Invalid rule definition for', def.type, name); + + let method = rule.method; + if (method === undefined) { + method = function () { + + return this.addRule(name); + }; + } + + if (method) { + Hoek.assert(!prototype[name], 'Rule conflict in', def.type, name); + prototype[name] = method; + } + + Hoek.assert(!rules[name], 'Rule conflict in', def.type, name); + rules[name] = rule; + + if (rule.alias) { + const aliases = [].concat(rule.alias); + for (const alias of aliases) { + prototype[alias] = rule.method; + } + } + + if (rule.args) { + rule.argsByName = new Map(); + rule.args = rule.args.map((arg) => { + + if (typeof arg === 'string') { + arg = { name: arg }; + } + + Hoek.assert(!rule.argsByName.has(arg.name), 'Duplicated argument name', arg.name); + + rule.argsByName.set(arg.name, arg); + return arg; + }); + } + } + } + + def.rules = rules; + + // Overrides + + if (def.overrides) { + prototype.super = base; + Object.assign(prototype, def.overrides); + } + + // Casts + + def.cast = Object.assign({}, parent.cast, def.cast); + + // Manifest + + def.build = internals.build(def.build, parent.build); + + // Modify + + def.modify = internals.modify(def.modify, parent.modify); + def.rebuild = internals.rebuild(def.rebuild, parent.rebuild); + + schema._ids._reachable = !!def.modify; + + return schema; +}; + + +// Helpers + +internals.build = function (child, parent) { + + if (!child || + !parent) { + + return child || parent; + } + + return function (obj, desc) { + + return child(parent(obj, desc), desc); + }; +}; + + +internals.coerce = function (child, parent) { + + if (!child || + !parent) { + + return child || parent; + } + + return { + from: child.from && parent.from ? [...new Set([...child.from, ...parent.from])] : null, + method: function (schema, value, helpers) { + + let coerced; + if (!parent.from || + parent.from.includes(typeof value)) { + + coerced = parent.method(schema, value, helpers); + if (coerced) { + if (coerced.errors) { + return coerced; + } + + value = coerced.value; + } + } + + if (value !== undefined && + (!child.from || child.from.includes(typeof value))) { + + const own = child.method(schema, value, helpers); + if (own) { + if (own.errors) { + return own; + } + + coerced = own; + } + } + + return coerced; + } + }; +}; + + +internals.modify = function (child, parent) { + + if (!child || + !parent) { + + return child || parent; + } + + return function (obj, id, schema) { + + const found = parent(obj, id, schema); + if (found) { + return found; + } + + return child(obj, id, schema); + }; +}; + + +internals.rebuild = function (child, parent) { + + if (!child || + !parent) { + + return child || parent; + } + + return function (schema) { + + parent(schema); + child(schema); + }; +}; + + +internals.validate = function (child, parent) { + + if (!child || + !parent) { + + return child || parent; + } + + return function (schema, value, helpers) { + + const result = parent(schema, value, helpers); + if (result) { + if (result.errors && + (!Array.isArray(result.errors) || result.errors.length)) { + + return result; + } + + value = result.value; + } + + return child(schema, value, helpers); + }; +}; diff --git a/lib/manifest.js b/lib/manifest.js index ecd73cbf5..3e38dbfec 100755 --- a/lib/manifest.js +++ b/lib/manifest.js @@ -233,6 +233,7 @@ internals.Builder = class { // Type let schema = this.joi[desc.type](); + const def = schema._definition; // Flags @@ -272,7 +273,7 @@ internals.Builder = class { } const keys = Object.keys(built); - const definition = schema._definition.rules[rule.name].args; + const definition = def.rules[rule.name].args; if (definition) { Hoek.assert(keys.length <= definition.length, 'Invalid number of arguments for', desc.type, rule.name, '(expected up to', definition.length, ', found', keys.length, ')'); for (const { name } of definition) { @@ -353,10 +354,10 @@ internals.Builder = class { } } - if (typeof schema._build === 'function' && + if (def.build && Object.keys(inners).length) { - schema = schema._build(inners); + schema = def.build(schema, inners); } return schema; diff --git a/lib/modify.js b/lib/modify.js index 7e1edc31d..b6dd4cd5f 100755 --- a/lib/modify.js +++ b/lib/modify.js @@ -50,7 +50,8 @@ exports.Ids = internals.Ids = class { Hoek.assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type'); for (const node of chain) { - adjusted = { id: node.id, schema: node.schema._override(adjusted.id, adjusted.schema) }; + const def = node.schema._definition; + adjusted = { id: node.id, schema: def.modify(node.schema, adjusted.id, adjusted.schema) }; } return adjusted.schema; @@ -135,7 +136,7 @@ exports.Ids = internals.Ids = class { return nodes; } - Hoek.assert(typeof node.schema._override === 'function', 'Schema node', [...behind, ...path].join('.'), 'does not support manipulation'); + Hoek.assert(node.schema._definition.modify, 'Schema node', [...behind, ...path].join('.'), 'does not support manipulation'); return node.schema._ids._collect(forward, [...behind, current], nodes); } }; diff --git a/lib/schemas.js b/lib/schemas.js index 0a27a8e8d..0bbde6578 100755 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -67,14 +67,19 @@ internals.rule = Joi.object({ exports.extension = Joi.object({ type: Joi.string().required(), - base: Joi.object().schema(), + args: Joi.func(), + base: Joi.object().schema(), + build: Joi.func().arity(2), coerce: Joi.func().maxArity(3), - validate: Joi.func().maxArity(3), + initialize: Joi.func().arity(0), messages: [Joi.object(), Joi.string()], - rules: Joi.object() - .pattern(internals.nameRx, internals.rule) + modify: Joi.func().arity(3), + rebuild: Joi.func().arity(0), + rules: Joi.object().pattern(internals.nameRx, internals.rule), + validate: Joi.func().maxArity(3) }) + .and('modify', 'rebuild') .strict(); diff --git a/lib/types/alternatives.js b/lib/types/alternatives.js index e117aab97..4e7093f02 100755 --- a/lib/types/alternatives.js +++ b/lib/types/alternatives.js @@ -16,10 +16,6 @@ module.exports = Any.extend({ type: 'alternatives', - properties: { - reachable: true - }, - // Initialize initialize: function () { @@ -113,7 +109,7 @@ module.exports = Any.extend({ obj._inners.matches.push({ schema: obj._cast(schema) }); } - return obj._rebuild(); + return obj.rebuild(); } } }, @@ -163,7 +159,7 @@ module.exports = Any.extend({ } } - return obj._rebuild(); + return obj.rebuild(); }, when(condition, options) { @@ -220,7 +216,7 @@ module.exports = Any.extend({ if (options.switch === undefined) { obj._inners.matches.push(normalize(condition, options)); - return obj._rebuild(); + return obj.rebuild(); } // Switch statement @@ -246,15 +242,13 @@ module.exports = Any.extend({ } } - return obj._rebuild(); + return obj.rebuild(); } }, // Build - build: function (desc) { - - let obj = this; // eslint-disable-line consistent-this + build: function (obj, desc) { for (const { schema, ref, peek, is, then, otherwise } of desc.matches) { if (schema) { @@ -270,17 +264,17 @@ module.exports = Any.extend({ // Modify - modify: function (id, schema) { + modify: function (schema, id, replacement) { let i = 0; - for (const match of this._inners.matches) { + for (const match of schema._inners.matches) { for (const key of ['schema', 'peek', 'is', 'then', 'otherwise']) { if (match[key] && id === match[key]._flags.id) { - const obj = this.clone(); - obj._inners.matches[i] = Object.assign({}, match, { [key]: schema }); - return obj._rebuild(); + const obj = schema.clone(); + obj._inners.matches[i] = Object.assign({}, match, { [key]: replacement }); + return obj.rebuild(); } } @@ -288,15 +282,11 @@ module.exports = Any.extend({ } }, - rebuild: function () { - - Hoek.assert(!this._inRuleset(), 'Cannot set alternative schemas inside a ruleset'); - - this._resetRegistrations(); + rebuild: function (schema) { - for (const match of this._inners.matches) { + for (const match of schema._inners.matches) { for (const key of ['schema', 'ref', 'peek', 'is', 'then', 'otherwise']) { - this._register(match[key], { family: Ref.toSibling }); + schema._register(match[key], { family: Ref.toSibling }); } // Flag when an alternative type is an array @@ -305,14 +295,13 @@ module.exports = Any.extend({ if (match[key] && match[key]._type === 'array') { - this.setFlag('_arrayItems', true, { clone: false }); + schema.setFlag('_arrayItems', true, { clone: false }); break; } } } - this._ruleset = false; - return this; + return schema; }, // Errors diff --git a/lib/types/array.js b/lib/types/array.js index ea7e7573b..07361464b 100755 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -15,10 +15,6 @@ module.exports = Any.extend({ type: 'array', - properties: { - reachable: true - }, - // Initialize initialize: function () { @@ -132,7 +128,7 @@ module.exports = Any.extend({ obj._inners.items.push(type); } - return obj._rebuild(); + return obj.rebuild(); }, validate: function (value, { schema, error, state, prefs }) { @@ -407,7 +403,7 @@ module.exports = Any.extend({ obj._inners.ordered.push(type); } - return obj._rebuild(); + return obj.rebuild(); } }, @@ -565,9 +561,9 @@ module.exports = Any.extend({ // Cast cast: { - from: Array.isArray, - to: { - set: function (value, options) { + set: { + from: Array.isArray, + to: function (value, helpers) { return new Set(value); } @@ -576,9 +572,7 @@ module.exports = Any.extend({ // Build - build: function (desc) { - - let obj = this; // eslint-disable-line consistent-this + build: function (obj, desc) { if (desc.items) { obj = obj.items(...desc.items); @@ -593,66 +587,64 @@ module.exports = Any.extend({ // Modify - modify: function (id, schema) { + modify: function (schema, id, replacement) { for (const set of ['items', 'ordered']) { - for (let i = 0; i < this._inners[set].length; ++i) { - const existing = this._inners[set][i]; + for (let i = 0; i < schema._inners[set].length; ++i) { + const existing = schema._inners[set][i]; if (id === existing._flags.id) { - const obj = this.clone(); - obj._inners[set][i] = schema; - obj._rebuild(); + const obj = schema.clone(); + obj._inners[set][i] = replacement; + obj.rebuild(); return obj; } } } - const hases = this.getRules('has'); + const hases = schema.getRules('has'); for (const has of hases) { if (id === has.args.schema._flags.id) { - const obj = this.clone(); - has.args.schema = schema; - obj._rebuild(); + const obj = schema.clone(); + has.args.schema = replacement; + obj.rebuild(); return obj; } } }, - rebuild: function () { - - this._resetRegistrations(); + rebuild: function (schema) { - this._inners._inclusions = []; - this._inners._exclusions = []; - this._inners._requireds = []; + schema._inners._inclusions = []; + schema._inners._exclusions = []; + schema._inners._requireds = []; - for (const type of this._inners.items) { - internals.validateSingle(type, this); + for (const type of schema._inners.items) { + internals.validateSingle(type, schema); - this._register(type); + schema._register(type); if (type._flags.presence === 'required') { - this._inners._requireds.push(type); + schema._inners._requireds.push(type); } else if (type._flags.presence === 'forbidden') { - this._inners._exclusions.push(type.optional()); + schema._inners._exclusions.push(type.optional()); } else { - this._inners._inclusions.push(type); + schema._inners._inclusions.push(type); } } - for (const type of this._inners.ordered) { - internals.validateSingle(type, this); - this._register(type); + for (const type of schema._inners.ordered) { + internals.validateSingle(type, schema); + schema._register(type); } - const hases = this.getRules('has'); + const hases = schema.getRules('has'); for (const has of hases) { - this._register(has.args.schema); + schema._register(has.args.schema); } - return this; + return schema; }, // Errors diff --git a/lib/types/binary.js b/lib/types/binary.js index bf23488da..163633af3 100755 --- a/lib/types/binary.js +++ b/lib/types/binary.js @@ -88,9 +88,9 @@ module.exports = Any.extend({ // Cast cast: { - from: (value) => Buffer.isBuffer(value), - to: { - string: function (value, options) { + string: { + from: (value) => Buffer.isBuffer(value), + to: function (value, helpers) { return value.toString(); } diff --git a/lib/types/boolean.js b/lib/types/boolean.js index 9c843d339..eef243c62 100755 --- a/lib/types/boolean.js +++ b/lib/types/boolean.js @@ -10,6 +10,12 @@ const Values = require('../values'); const internals = {}; +internals.isBool = function (value) { + + return typeof value === 'boolean'; +}; + + module.exports = Any.extend({ type: 'boolean', @@ -101,14 +107,16 @@ module.exports = Any.extend({ // Cast cast: { - from: (value) => typeof value === 'boolean', - to: { - number: function (value) { + number: { + from: internals.isBool, + to: function (value, helpers) { return value ? 1 : 0; - }, - - string: function (value, options) { + } + }, + string: { + from: internals.isBool, + to: function (value, helpers) { return value ? 'true' : 'false'; } @@ -117,9 +125,7 @@ module.exports = Any.extend({ // Build - build: function (desc) { - - let obj = this; // eslint-disable-line consistent-this + build: function (obj, desc) { if (desc.truthy) { obj = obj.truthy(...desc.truthy); diff --git a/lib/types/date.js b/lib/types/date.js index e5dcc02c1..c8b236b76 100755 --- a/lib/types/date.js +++ b/lib/types/date.js @@ -10,6 +10,12 @@ const Template = require('../template'); const internals = {}; +internals.isDate = function (value) { + + return value instanceof Date; +}; + + module.exports = Any.extend({ type: 'date', @@ -129,14 +135,16 @@ module.exports = Any.extend({ // Cast cast: { - from: (value) => value instanceof Date, - to: { - number: function (value) { + number: { + from: internals.isDate, + to: function (value, helpers) { return value.getTime(); - }, - - string: function (value, { prefs }) { + } + }, + string: { + from: internals.isDate, + to: function (value, { prefs }) { return Template.date(value, prefs); } diff --git a/lib/types/function.js b/lib/types/function.js index fcfec8cf8..c09ca5bef 100755 --- a/lib/types/function.js +++ b/lib/types/function.js @@ -89,9 +89,7 @@ module.exports = ObjectType.extend({ // Cast cast: { - to: { - map: null // Disable object cast - } + map: null // Disable object cast }, // Errors diff --git a/lib/types/link.js b/lib/types/link.js index 1d0e990cd..db2fd3aeb 100755 --- a/lib/types/link.js +++ b/lib/types/link.js @@ -106,9 +106,9 @@ module.exports = Any.extend({ // Build - build: function (desc) { + build: function (obj, desc) { - return this.ref(desc.link); + return obj.ref(desc.link); }, // Errors diff --git a/lib/types/number.js b/lib/types/number.js index 1067703b3..c40844cf9 100755 --- a/lib/types/number.js +++ b/lib/types/number.js @@ -270,9 +270,9 @@ module.exports = Any.extend({ // Cast cast: { - from: (value) => typeof value === 'number', - to: { - string: function (value, options) { + string: { + from: (value) => typeof value === 'number', + to: function (value, helpers) { return value.toString(); } diff --git a/lib/types/object.js b/lib/types/object.js index ee718b407..8a4065d9b 100755 --- a/lib/types/object.js +++ b/lib/types/object.js @@ -26,7 +26,6 @@ module.exports = Any.extend({ type: 'object', properties: { - reachable: true, typeof: 'object' }, @@ -273,7 +272,7 @@ module.exports = Any.extend({ } } - return obj._rebuild(); + return obj.rebuild(); } }, @@ -512,16 +511,16 @@ module.exports = Any.extend({ } } - return obj._rebuild(); + return obj.rebuild(); } }, // Cast cast: { - from: (value) => value && typeof value === 'object', - to: { - map: function (value, options) { + map: { + from: (value) => value && typeof value === 'object', + to: function (value, helpers) { return new Map(Object.entries(value)); } @@ -530,9 +529,7 @@ module.exports = Any.extend({ // Build - build: function (desc) { - - let obj = this; // eslint-disable-line consistent-this + build: function (obj, desc) { if (desc.keys) { obj = obj.keys(desc.keys); @@ -561,80 +558,76 @@ module.exports = Any.extend({ // Modify - modify: function (id, schema) { + modify: function (schema, id, replacement) { - if (this._inners.keys) { - for (const child of this._inners.keys) { + if (schema._inners.keys) { + for (const child of schema._inners.keys) { const childId = child.schema._flags.id || child.key; if (id === childId) { - return this.keys({ [child.key]: schema }); + return schema.keys({ [child.key]: replacement }); } } } - if (this._inners.patterns) { - for (let i = 0; i < this._inners.patterns.length; ++i) { - const pattern = this._inners.patterns[i]; + if (schema._inners.patterns) { + for (let i = 0; i < schema._inners.patterns.length; ++i) { + const pattern = schema._inners.patterns[i]; for (const key of ['schema', 'rule', 'matches']) { if (pattern[key] && id === pattern[key]._flags.id) { - const obj = this.clone(); - obj._inners.patterns[i] = Object.assign({}, pattern, { [key]: schema }); - return obj._rebuild(); + const obj = schema.clone(); + obj._inners.patterns[i] = Object.assign({}, pattern, { [key]: replacement }); + return obj.rebuild(); } } } } let i = 0; - for (const rule of this._rules) { + for (const rule of schema._rules) { if (rule.name === 'assert' && id === rule.options.args.schema._flags.id) { - const obj = this.clone(); + const obj = schema.clone(); const clone = Hoek.clone(rule); - clone.options.args.schema = schema; + clone.options.args.schema = replacement; clone.args = clone.options.args; obj._rules[i] = clone; - return obj._rebuild(); + return obj.rebuild(); } ++i; } }, - rebuild: function () { - - this._resetRegistrations(); + rebuild: function (schema) { - if (this._inners.keys) { + if (schema._inners.keys) { const topo = new Topo(); - for (const child of this._inners.keys) { - const { schema, key } = child; - Common.tryWithPath(() => topo.add(child, { after: schema._refs.roots(), group: key }), key); - this._register(schema, { key }); + for (const child of schema._inners.keys) { + Common.tryWithPath(() => topo.add(child, { after: child.schema._refs.roots(), group: child.key }), child.key); + schema._register(child.schema, { key: child.key }); } - this._inners.keys = new internals.Keys(...topo.nodes); + schema._inners.keys = new internals.Keys(...topo.nodes); } - if (this._inners.patterns) { - for (const pattern of this._inners.patterns) { + if (schema._inners.patterns) { + for (const pattern of schema._inners.patterns) { for (const key of ['schema', 'rule', 'matches']) { - this._register(pattern[key]); + schema._register(pattern[key]); } } } - const assertions = this.getRules('assert'); + const assertions = schema.getRules('assert'); for (const assertion of assertions) { - this._register(assertion.args.schema); + schema._register(assertion.args.schema); } - this._ruleset = false; - return this; + return schema; }, // Errors diff --git a/lib/types/string/index.js b/lib/types/string/index.js index 5c4f0c210..161bbab8d 100755 --- a/lib/types/string/index.js +++ b/lib/types/string/index.js @@ -723,9 +723,7 @@ module.exports = Any.extend({ // Build - build: function (desc) { - - let obj = this; // eslint-disable-line consistent-this + build: function (obj, desc) { for (const { pattern, replacement } of desc.replacements) { obj = obj.replace(pattern, replacement); diff --git a/lib/types/symbol.js b/lib/types/symbol.js index dfa8a736f..c030f5e2c 100755 --- a/lib/types/symbol.js +++ b/lib/types/symbol.js @@ -93,9 +93,8 @@ module.exports = Any.extend({ // Build - build: function (desc) { + build: function (obj, desc) { - let obj = this; // eslint-disable-line consistent-this obj = obj.map(desc.map); return obj; }, diff --git a/lib/validator.js b/lib/validator.js index 43698039f..0e630229d 100755 --- a/lib/validator.js +++ b/lib/validator.js @@ -150,7 +150,7 @@ exports.validate = function (value, schema, state, prefs) { const coerced = def.coerce.method(schema, value, helpers); if (coerced) { if (coerced.errors) { - return internals.finalize(coerced.value, schema, original, [].concat(coerced.errors), state, prefs); // Coerced error always aborts early + return internals.finalize(coerced.value, original, [].concat(coerced.errors), helpers); // Coerced error always aborts early } value = coerced.value; @@ -170,23 +170,23 @@ exports.validate = function (value, schema, state, prefs) { const presence = schema._flags.presence || (schema._flags._endedSwitch ? 'ignore' : prefs.presence); if (value === undefined) { if (presence === 'forbidden') { - return internals.finalize(value, schema, original, null, state, prefs); + return internals.finalize(value, original, null, helpers); } if (presence === 'required') { - return internals.finalize(value, schema, original, [schema.createError('any.required', value, null, state, prefs)], state, prefs); + return internals.finalize(value, original, [schema.createError('any.required', value, null, state, prefs)], helpers); } if (presence === 'optional') { if (schema._flags.default !== Common.symbols.deepDefault) { - return internals.finalize(value, schema, original, null, state, prefs); + return internals.finalize(value, original, null, helpers); } value = {}; } } else if (presence === 'forbidden') { - return internals.finalize(value, schema, original, [schema.createError('any.unknown', value, null, state, prefs)], state, prefs); + return internals.finalize(value, original, [schema.createError('any.unknown', value, null, state, prefs)], helpers); } // Allowed values @@ -200,13 +200,13 @@ exports.validate = function (value, schema, state, prefs) { value = match.value; } - return internals.finalize(value, schema, original, null, state, prefs); + return internals.finalize(value, original, null, helpers); } if (schema._flags.only) { const report = schema.createError('any.only', value, { valids: schema._valids.values({ stripUndefined: true }) }, state, prefs); if (prefs.abortEarly) { - return internals.finalize(value, schema, original, [report], state, prefs); + return internals.finalize(value, original, [report], helpers); } errors.push(report); @@ -219,7 +219,7 @@ exports.validate = function (value, schema, state, prefs) { if (schema._invalids.has(value, state, prefs, schema._flags.insensitive)) { const report = schema.createError('any.invalid', value, { invalids: schema._invalids.values({ stripUndefined: true }) }, state, prefs); if (prefs.abortEarly) { - return internals.finalize(value, schema, original, [report], state, prefs); + return internals.finalize(value, original, [report], helpers); } errors.push(report); @@ -236,12 +236,12 @@ exports.validate = function (value, schema, state, prefs) { if (base.errors) { if (!Array.isArray(base.errors)) { errors.push(base.errors); - return internals.finalize(value, schema, original, errors, state, prefs); // Base error always aborts early + return internals.finalize(value, original, errors, helpers); // Base error always aborts early } if (base.errors.length) { errors.push(...base.errors); - return internals.finalize(value, schema, original, errors, state, prefs); // Base error always aborts early + return internals.finalize(value, original, errors, helpers); // Base error always aborts early } } } @@ -250,7 +250,7 @@ exports.validate = function (value, schema, state, prefs) { // Validate tests if (!schema._rules.length) { - return internals.finalize(value, schema, original, errors, state, prefs); + return internals.finalize(value, original, errors, helpers); } return internals.rules(value, errors, original, helpers); @@ -306,7 +306,7 @@ internals.rules = function (value, errors, original, helpers) { } if (prefs.abortEarly) { - return internals.finalize(value, schema, original, result.errors, state, prefs); + return internals.finalize(value, original, result.errors, helpers); } errors.push(...result.errors); @@ -316,7 +316,7 @@ internals.rules = function (value, errors, original, helpers) { } } - return internals.finalize(value, schema, original, errors, state, prefs); + return internals.finalize(value, original, errors, helpers); }; @@ -348,14 +348,15 @@ internals.error = function (report, { message }) { }; -internals.finalize = function (value, schema, original, errors, state, prefs) { +internals.finalize = function (value, original, errors, helpers) { errors = errors || []; + const { schema, state, prefs } = helpers; // Failover value if (errors.length) { - const failover = internals.default('failover', undefined, schema, errors, state, prefs); + const failover = internals.default('failover', undefined, errors, helpers); if (failover !== undefined) { value = failover; errors = []; @@ -381,16 +382,18 @@ internals.finalize = function (value, schema, original, errors, state, prefs) { // Default if (value === undefined) { - value = internals.default('default', value, schema, errors, state, prefs); + value = internals.default('default', value, errors, helpers); } // Cast if (schema._flags.cast && - value !== undefined && - schema._casts[Common.symbols.castFrom](value)) { + value !== undefined) { - value = schema._casts[schema._flags.cast](value, { schema, state, prefs }); + const caster = schema._definition.cast[schema._flags.cast]; + if (caster.from(value)) { + value = caster.to(value, helpers); + } } // Externals @@ -445,7 +448,7 @@ internals.prefs = function (schema, prefs) { }; -internals.default = function (flag, value, schema, errors, state, prefs) { +internals.default = function (flag, value, errors, { schema, state, prefs }) { const source = schema._flags[flag]; if (prefs.noDefaults || diff --git a/test/extend.js b/test/extend.js index f6a90ee3c..0b4a12b16 100755 --- a/test/extend.js +++ b/test/extend.js @@ -1187,4 +1187,89 @@ describe('extension', () => { }] ]); }); + + it('extends modify', () => { + + const custom = Joi.extend((joi) => { + + return { + type: 'special', + base: joi.object(), + initialize: function () { + + this._inners.tests = []; + }, + rules: { + test: { + method: function (schema) { + + const obj = this.clone(); + obj._inners.tests.push(schema); + obj._register(schema); + return obj; + } + } + }, + modify: function (schema, id, replacement) { + + for (let i = 0; i < schema._inners.tests.length; ++i) { + const item = schema._inners.tests[i]; + if (id === item._flags.id) { + const obj = schema.clone(); + obj._inners.tests[i] = replacement; + return obj.rebuild(); + } + } + }, + rebuild: function () { + + return this; + } + }; + }); + + const schema = custom.special().keys({ y: Joi.number() }).test(Joi.number().id('x')); + + const modified1 = schema.fork('x', (s) => s.min(10)); + expect(modified1.describe()).to.equal({ + type: 'special', + keys: { + y: { type: 'number' } + }, + tests: [ + { + type: 'number', + flags: { id: 'x' }, + rules: [ + { + name: 'min', + args: { limit: 10 } + } + ] + } + ] + }); + + const modified2 = schema.fork('y', (s) => s.min(10)); + expect(modified2.describe()).to.equal({ + type: 'special', + keys: { + y: { + type: 'number', + rules: [ + { + name: 'min', + args: { limit: 10 } + } + ] + } + }, + tests: [ + { + type: 'number', + flags: { id: 'x' } + } + ] + }); + }); }); diff --git a/test/manifest.js b/test/manifest.js index dd90839e3..5705be6f1 100755 --- a/test/manifest.js +++ b/test/manifest.js @@ -444,7 +444,51 @@ describe('Manifest', () => { ]); }); - it('builds extended schema', () => { + it('builds extended schema (nested builds)', () => { + + const custom = Joi.extend({ + type: 'fancy', + base: Joi.object({ a: Joi.number() }), + initialize: function () { + + this._inners.fancy = []; + }, + rules: { + pants: { + method: function (button) { + + this._inners.fancy.push(button); + return this; + } + } + }, + build: function (obj, desc) { + + if (desc.fancy) { + obj = obj.clone(); + obj._inners.fancy = desc.fancy.slice(); + } + + return obj; + } + }); + + const schema = custom.fancy().pants('green'); + const desc = schema.describe(); + + expect(desc).to.equal({ + type: 'fancy', + keys: { + a: { type: 'number' } + }, + fancy: ['green'] + }); + + const built = custom.build(desc); + expect(built).to.equal(schema, { skip: ['_ruleset'] }); + }); + + it('builds extended schema (complex)', () => { const custom = Joi.extend({ type: 'million',