313 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 | |
|     };
 | |
| };
 |