You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
7.4 KiB
312 lines
7.4 KiB
'use strict'; |
|
|
|
const Assert = require('@hapi/hoek/lib/assert'); |
|
const Clone = require('@hapi/hoek/lib/clone'); |
|
|
|
const Common = require('./common'); |
|
const Messages = require('./messages'); |
|
|
|
|
|
const internals = {}; |
|
|
|
|
|
exports.type = function (from, options) { |
|
|
|
const base = Object.getPrototypeOf(from); |
|
const prototype = Clone(base); |
|
const schema = from._assign(Object.create(prototype)); |
|
const def = Object.assign({}, options); // Shallow cloned |
|
delete def.base; |
|
|
|
prototype._definition = def; |
|
|
|
const parent = base._definition || {}; |
|
def.messages = Messages.merge(parent.messages, def.messages); |
|
def.properties = Object.assign({}, parent.properties, def.properties); |
|
|
|
// Type |
|
|
|
schema.type = def.type; |
|
|
|
// Flags |
|
|
|
def.flags = Object.assign({}, parent.flags, def.flags); |
|
|
|
// Terms |
|
|
|
const terms = Object.assign({}, parent.terms); |
|
if (def.terms) { |
|
for (const name in def.terms) { // Only apply own terms |
|
const term = def.terms[name]; |
|
Assert(schema.$_terms[name] === undefined, 'Invalid term override for', def.type, name); |
|
schema.$_terms[name] = term.init; |
|
terms[name] = term; |
|
} |
|
} |
|
|
|
def.terms = terms; |
|
|
|
// Constructor arguments |
|
|
|
if (!def.args) { |
|
def.args = parent.args; |
|
} |
|
|
|
// Prepare |
|
|
|
def.prepare = internals.prepare(def.prepare, parent.prepare); |
|
|
|
// 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); |
|
|
|
// Validate |
|
|
|
def.validate = internals.validate(def.validate, parent.validate); |
|
|
|
// Rules |
|
|
|
const rules = Object.assign({}, parent.rules); |
|
if (def.rules) { |
|
for (const name in def.rules) { |
|
const rule = def.rules[name]; |
|
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) { |
|
Assert(!prototype[name], 'Rule conflict in', def.type, name); |
|
prototype[name] = method; |
|
} |
|
|
|
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 }; |
|
} |
|
|
|
Assert(!rule.argsByName.has(arg.name), 'Duplicated argument name', arg.name); |
|
|
|
if (Common.isSchema(arg.assert)) { |
|
arg.assert = arg.assert.strict().label(arg.name); |
|
} |
|
|
|
rule.argsByName.set(arg.name, arg); |
|
return arg; |
|
}); |
|
} |
|
} |
|
} |
|
|
|
def.rules = rules; |
|
|
|
// Modifiers |
|
|
|
const modifiers = Object.assign({}, parent.modifiers); |
|
if (def.modifiers) { |
|
for (const name in def.modifiers) { |
|
Assert(!prototype[name], 'Rule conflict in', def.type, name); |
|
|
|
const modifier = def.modifiers[name]; |
|
Assert(typeof modifier === 'function', 'Invalid modifier definition for', def.type, name); |
|
|
|
const method = function (arg) { |
|
|
|
return this.rule({ [name]: arg }); |
|
}; |
|
|
|
prototype[name] = method; |
|
modifiers[name] = modifier; |
|
} |
|
} |
|
|
|
def.modifiers = modifiers; |
|
|
|
// Overrides |
|
|
|
if (def.overrides) { |
|
prototype._super = base; |
|
schema.$_super = {}; // Backwards compatibility |
|
for (const override in def.overrides) { |
|
Assert(base[override], 'Cannot override missing', override); |
|
def.overrides[override][Common.symbols.parent] = base[override]; |
|
schema.$_super[override] = base[override].bind(schema); // Backwards compatibility |
|
} |
|
|
|
Object.assign(prototype, def.overrides); |
|
} |
|
|
|
// Casts |
|
|
|
def.cast = Object.assign({}, parent.cast, def.cast); |
|
|
|
// Manifest |
|
|
|
const manifest = Object.assign({}, parent.manifest, def.manifest); |
|
manifest.build = internals.build(def.manifest && def.manifest.build, parent.manifest && parent.manifest.build); |
|
def.manifest = manifest; |
|
|
|
// Rebuild |
|
|
|
def.rebuild = internals.rebuild(def.rebuild, parent.rebuild); |
|
|
|
return schema; |
|
}; |
|
|
|
|
|
// Helpers |
|
|
|
internals.build = function (child, parent) { |
|
|
|
if (!child || |
|
!parent) { |
|
|
|
return child || parent; |
|
} |
|
|
|
return function (obj, desc) { |
|
|
|
return parent(child(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(value, helpers) { |
|
|
|
let coerced; |
|
if (!parent.from || |
|
parent.from.includes(typeof value)) { |
|
|
|
coerced = parent.method(value, helpers); |
|
if (coerced) { |
|
if (coerced.errors || |
|
coerced.value === undefined) { |
|
|
|
return coerced; |
|
} |
|
|
|
value = coerced.value; |
|
} |
|
} |
|
|
|
if (!child.from || |
|
child.from.includes(typeof value)) { |
|
|
|
const own = child.method(value, helpers); |
|
if (own) { |
|
return own; |
|
} |
|
} |
|
|
|
return coerced; |
|
} |
|
}; |
|
}; |
|
|
|
|
|
internals.prepare = function (child, parent) { |
|
|
|
if (!child || |
|
!parent) { |
|
|
|
return child || parent; |
|
} |
|
|
|
return function (value, helpers) { |
|
|
|
const prepared = child(value, helpers); |
|
if (prepared) { |
|
if (prepared.errors || |
|
prepared.value === undefined) { |
|
|
|
return prepared; |
|
} |
|
|
|
value = prepared.value; |
|
} |
|
|
|
return parent(value, helpers) || prepared; |
|
}; |
|
}; |
|
|
|
|
|
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 (value, helpers) { |
|
|
|
const result = parent(value, helpers); |
|
if (result) { |
|
if (result.errors && |
|
(!Array.isArray(result.errors) || result.errors.length)) { |
|
|
|
return result; |
|
} |
|
|
|
value = result.value; |
|
} |
|
|
|
return child(value, helpers) || result; |
|
}; |
|
};
|
|
|