extend.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const Clone = require('@hapi/hoek/lib/clone');
  4. const Common = require('./common');
  5. const Messages = require('./messages');
  6. const internals = {};
  7. exports.type = function (from, options) {
  8. const base = Object.getPrototypeOf(from);
  9. const prototype = Clone(base);
  10. const schema = from._assign(Object.create(prototype));
  11. const def = Object.assign({}, options); // Shallow cloned
  12. delete def.base;
  13. prototype._definition = def;
  14. const parent = base._definition || {};
  15. def.messages = Messages.merge(parent.messages, def.messages);
  16. def.properties = Object.assign({}, parent.properties, def.properties);
  17. // Type
  18. schema.type = def.type;
  19. // Flags
  20. def.flags = Object.assign({}, parent.flags, def.flags);
  21. // Terms
  22. const terms = Object.assign({}, parent.terms);
  23. if (def.terms) {
  24. for (const name in def.terms) { // Only apply own terms
  25. const term = def.terms[name];
  26. Assert(schema.$_terms[name] === undefined, 'Invalid term override for', def.type, name);
  27. schema.$_terms[name] = term.init;
  28. terms[name] = term;
  29. }
  30. }
  31. def.terms = terms;
  32. // Constructor arguments
  33. if (!def.args) {
  34. def.args = parent.args;
  35. }
  36. // Prepare
  37. def.prepare = internals.prepare(def.prepare, parent.prepare);
  38. // Coerce
  39. if (def.coerce) {
  40. if (typeof def.coerce === 'function') {
  41. def.coerce = { method: def.coerce };
  42. }
  43. if (def.coerce.from &&
  44. !Array.isArray(def.coerce.from)) {
  45. def.coerce = { method: def.coerce.method, from: [].concat(def.coerce.from) };
  46. }
  47. }
  48. def.coerce = internals.coerce(def.coerce, parent.coerce);
  49. // Validate
  50. def.validate = internals.validate(def.validate, parent.validate);
  51. // Rules
  52. const rules = Object.assign({}, parent.rules);
  53. if (def.rules) {
  54. for (const name in def.rules) {
  55. const rule = def.rules[name];
  56. Assert(typeof rule === 'object', 'Invalid rule definition for', def.type, name);
  57. let method = rule.method;
  58. if (method === undefined) {
  59. method = function () {
  60. return this.$_addRule(name);
  61. };
  62. }
  63. if (method) {
  64. Assert(!prototype[name], 'Rule conflict in', def.type, name);
  65. prototype[name] = method;
  66. }
  67. Assert(!rules[name], 'Rule conflict in', def.type, name);
  68. rules[name] = rule;
  69. if (rule.alias) {
  70. const aliases = [].concat(rule.alias);
  71. for (const alias of aliases) {
  72. prototype[alias] = rule.method;
  73. }
  74. }
  75. if (rule.args) {
  76. rule.argsByName = new Map();
  77. rule.args = rule.args.map((arg) => {
  78. if (typeof arg === 'string') {
  79. arg = { name: arg };
  80. }
  81. Assert(!rule.argsByName.has(arg.name), 'Duplicated argument name', arg.name);
  82. if (Common.isSchema(arg.assert)) {
  83. arg.assert = arg.assert.strict().label(arg.name);
  84. }
  85. rule.argsByName.set(arg.name, arg);
  86. return arg;
  87. });
  88. }
  89. }
  90. }
  91. def.rules = rules;
  92. // Modifiers
  93. const modifiers = Object.assign({}, parent.modifiers);
  94. if (def.modifiers) {
  95. for (const name in def.modifiers) {
  96. Assert(!prototype[name], 'Rule conflict in', def.type, name);
  97. const modifier = def.modifiers[name];
  98. Assert(typeof modifier === 'function', 'Invalid modifier definition for', def.type, name);
  99. const method = function (arg) {
  100. return this.rule({ [name]: arg });
  101. };
  102. prototype[name] = method;
  103. modifiers[name] = modifier;
  104. }
  105. }
  106. def.modifiers = modifiers;
  107. // Overrides
  108. if (def.overrides) {
  109. prototype._super = base;
  110. schema.$_super = {}; // Backwards compatibility
  111. for (const override in def.overrides) {
  112. Assert(base[override], 'Cannot override missing', override);
  113. def.overrides[override][Common.symbols.parent] = base[override];
  114. schema.$_super[override] = base[override].bind(schema); // Backwards compatibility
  115. }
  116. Object.assign(prototype, def.overrides);
  117. }
  118. // Casts
  119. def.cast = Object.assign({}, parent.cast, def.cast);
  120. // Manifest
  121. const manifest = Object.assign({}, parent.manifest, def.manifest);
  122. manifest.build = internals.build(def.manifest && def.manifest.build, parent.manifest && parent.manifest.build);
  123. def.manifest = manifest;
  124. // Rebuild
  125. def.rebuild = internals.rebuild(def.rebuild, parent.rebuild);
  126. return schema;
  127. };
  128. // Helpers
  129. internals.build = function (child, parent) {
  130. if (!child ||
  131. !parent) {
  132. return child || parent;
  133. }
  134. return function (obj, desc) {
  135. return parent(child(obj, desc), desc);
  136. };
  137. };
  138. internals.coerce = function (child, parent) {
  139. if (!child ||
  140. !parent) {
  141. return child || parent;
  142. }
  143. return {
  144. from: child.from && parent.from ? [...new Set([...child.from, ...parent.from])] : null,
  145. method(value, helpers) {
  146. let coerced;
  147. if (!parent.from ||
  148. parent.from.includes(typeof value)) {
  149. coerced = parent.method(value, helpers);
  150. if (coerced) {
  151. if (coerced.errors ||
  152. coerced.value === undefined) {
  153. return coerced;
  154. }
  155. value = coerced.value;
  156. }
  157. }
  158. if (!child.from ||
  159. child.from.includes(typeof value)) {
  160. const own = child.method(value, helpers);
  161. if (own) {
  162. return own;
  163. }
  164. }
  165. return coerced;
  166. }
  167. };
  168. };
  169. internals.prepare = function (child, parent) {
  170. if (!child ||
  171. !parent) {
  172. return child || parent;
  173. }
  174. return function (value, helpers) {
  175. const prepared = child(value, helpers);
  176. if (prepared) {
  177. if (prepared.errors ||
  178. prepared.value === undefined) {
  179. return prepared;
  180. }
  181. value = prepared.value;
  182. }
  183. return parent(value, helpers) || prepared;
  184. };
  185. };
  186. internals.rebuild = function (child, parent) {
  187. if (!child ||
  188. !parent) {
  189. return child || parent;
  190. }
  191. return function (schema) {
  192. parent(schema);
  193. child(schema);
  194. };
  195. };
  196. internals.validate = function (child, parent) {
  197. if (!child ||
  198. !parent) {
  199. return child || parent;
  200. }
  201. return function (value, helpers) {
  202. const result = parent(value, helpers);
  203. if (result) {
  204. if (result.errors &&
  205. (!Array.isArray(result.errors) || result.errors.length)) {
  206. return result;
  207. }
  208. value = result.value;
  209. }
  210. return child(value, helpers) || result;
  211. };
  212. };