validator.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const Clone = require('@hapi/hoek/lib/clone');
  4. const Ignore = require('@hapi/hoek/lib/ignore');
  5. const Reach = require('@hapi/hoek/lib/reach');
  6. const Common = require('./common');
  7. const Errors = require('./errors');
  8. const State = require('./state');
  9. const internals = {
  10. result: Symbol('result')
  11. };
  12. exports.entry = function (value, schema, prefs) {
  13. let settings = Common.defaults;
  14. if (prefs) {
  15. Assert(prefs.warnings === undefined, 'Cannot override warnings preference in synchronous validation');
  16. Assert(prefs.artifacts === undefined, 'Cannot override artifacts preference in synchronous validation');
  17. settings = Common.preferences(Common.defaults, prefs);
  18. }
  19. const result = internals.entry(value, schema, settings);
  20. Assert(!result.mainstay.externals.length, 'Schema with external rules must use validateAsync()');
  21. const outcome = { value: result.value };
  22. if (result.error) {
  23. outcome.error = result.error;
  24. }
  25. if (result.mainstay.warnings.length) {
  26. outcome.warning = Errors.details(result.mainstay.warnings);
  27. }
  28. if (result.mainstay.debug) {
  29. outcome.debug = result.mainstay.debug;
  30. }
  31. if (result.mainstay.artifacts) {
  32. outcome.artifacts = result.mainstay.artifacts;
  33. }
  34. return outcome;
  35. };
  36. exports.entryAsync = async function (value, schema, prefs) {
  37. let settings = Common.defaults;
  38. if (prefs) {
  39. settings = Common.preferences(Common.defaults, prefs);
  40. }
  41. const result = internals.entry(value, schema, settings);
  42. const mainstay = result.mainstay;
  43. if (result.error) {
  44. if (mainstay.debug) {
  45. result.error.debug = mainstay.debug;
  46. }
  47. throw result.error;
  48. }
  49. if (mainstay.externals.length) {
  50. let root = result.value;
  51. const errors = [];
  52. for (const external of mainstay.externals) {
  53. const path = external.state.path;
  54. const linked = external.schema.type === 'link' ? mainstay.links.get(external.schema) : null;
  55. let node = root;
  56. let key;
  57. let parent;
  58. const ancestors = path.length ? [root] : [];
  59. const original = path.length ? Reach(value, path) : value;
  60. if (path.length) {
  61. key = path[path.length - 1];
  62. let current = root;
  63. for (const segment of path.slice(0, -1)) {
  64. current = current[segment];
  65. ancestors.unshift(current);
  66. }
  67. parent = ancestors[0];
  68. node = parent[key];
  69. }
  70. try {
  71. const createError = (code, local) => (linked || external.schema).$_createError(code, node, local, external.state, settings);
  72. const output = await external.method(node, {
  73. schema: external.schema,
  74. linked,
  75. state: external.state,
  76. prefs,
  77. original,
  78. error: createError,
  79. errorsArray: internals.errorsArray,
  80. warn: (code, local) => mainstay.warnings.push((linked || external.schema).$_createError(code, node, local, external.state, settings)),
  81. message: (messages, local) => (linked || external.schema).$_createError('external', node, local, external.state, settings, { messages })
  82. });
  83. if (output === undefined ||
  84. output === node) {
  85. continue;
  86. }
  87. if (output instanceof Errors.Report) {
  88. mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
  89. errors.push(output);
  90. if (settings.abortEarly) {
  91. break;
  92. }
  93. continue;
  94. }
  95. if (Array.isArray(output) &&
  96. output[Common.symbols.errors]) {
  97. mainstay.tracer.log(external.schema, external.state, 'rule', 'external', 'error');
  98. errors.push(...output);
  99. if (settings.abortEarly) {
  100. break;
  101. }
  102. continue;
  103. }
  104. if (parent) {
  105. mainstay.tracer.value(external.state, 'rule', node, output, 'external');
  106. parent[key] = output;
  107. }
  108. else {
  109. mainstay.tracer.value(external.state, 'rule', root, output, 'external');
  110. root = output;
  111. }
  112. }
  113. catch (err) {
  114. if (settings.errors.label) {
  115. err.message += ` (${(external.label)})`; // Change message to include path
  116. }
  117. throw err;
  118. }
  119. }
  120. result.value = root;
  121. if (errors.length) {
  122. result.error = Errors.process(errors, value, settings);
  123. if (mainstay.debug) {
  124. result.error.debug = mainstay.debug;
  125. }
  126. throw result.error;
  127. }
  128. }
  129. if (!settings.warnings &&
  130. !settings.debug &&
  131. !settings.artifacts) {
  132. return result.value;
  133. }
  134. const outcome = { value: result.value };
  135. if (mainstay.warnings.length) {
  136. outcome.warning = Errors.details(mainstay.warnings);
  137. }
  138. if (mainstay.debug) {
  139. outcome.debug = mainstay.debug;
  140. }
  141. if (mainstay.artifacts) {
  142. outcome.artifacts = mainstay.artifacts;
  143. }
  144. return outcome;
  145. };
  146. internals.Mainstay = class {
  147. constructor(tracer, debug, links) {
  148. this.externals = [];
  149. this.warnings = [];
  150. this.tracer = tracer;
  151. this.debug = debug;
  152. this.links = links;
  153. this.shadow = null;
  154. this.artifacts = null;
  155. this._snapshots = [];
  156. }
  157. snapshot() {
  158. this._snapshots.push({
  159. externals: this.externals.slice(),
  160. warnings: this.warnings.slice()
  161. });
  162. }
  163. restore() {
  164. const snapshot = this._snapshots.pop();
  165. this.externals = snapshot.externals;
  166. this.warnings = snapshot.warnings;
  167. }
  168. commit() {
  169. this._snapshots.pop();
  170. }
  171. };
  172. internals.entry = function (value, schema, prefs) {
  173. // Prepare state
  174. const { tracer, cleanup } = internals.tracer(schema, prefs);
  175. const debug = prefs.debug ? [] : null;
  176. const links = schema._ids._schemaChain ? new Map() : null;
  177. const mainstay = new internals.Mainstay(tracer, debug, links);
  178. const schemas = schema._ids._schemaChain ? [{ schema }] : null;
  179. const state = new State([], [], { mainstay, schemas });
  180. // Validate value
  181. const result = exports.validate(value, schema, state, prefs);
  182. // Process value and errors
  183. if (cleanup) {
  184. schema.$_root.untrace();
  185. }
  186. const error = Errors.process(result.errors, value, prefs);
  187. return { value: result.value, error, mainstay };
  188. };
  189. internals.tracer = function (schema, prefs) {
  190. if (schema.$_root._tracer) {
  191. return { tracer: schema.$_root._tracer._register(schema) };
  192. }
  193. if (prefs.debug) {
  194. Assert(schema.$_root.trace, 'Debug mode not supported');
  195. return { tracer: schema.$_root.trace()._register(schema), cleanup: true };
  196. }
  197. return { tracer: internals.ignore };
  198. };
  199. exports.validate = function (value, schema, state, prefs, overrides = {}) {
  200. if (schema.$_terms.whens) {
  201. schema = schema._generate(value, state, prefs).schema;
  202. }
  203. // Setup state and settings
  204. if (schema._preferences) {
  205. prefs = internals.prefs(schema, prefs);
  206. }
  207. // Cache
  208. if (schema._cache &&
  209. prefs.cache) {
  210. const result = schema._cache.get(value);
  211. state.mainstay.tracer.debug(state, 'validate', 'cached', !!result);
  212. if (result) {
  213. return result;
  214. }
  215. }
  216. // Helpers
  217. const createError = (code, local, localState) => schema.$_createError(code, value, local, localState || state, prefs);
  218. const helpers = {
  219. original: value,
  220. prefs,
  221. schema,
  222. state,
  223. error: createError,
  224. errorsArray: internals.errorsArray,
  225. warn: (code, local, localState) => state.mainstay.warnings.push(createError(code, local, localState)),
  226. message: (messages, local) => schema.$_createError('custom', value, local, state, prefs, { messages })
  227. };
  228. // Prepare
  229. state.mainstay.tracer.entry(schema, state);
  230. const def = schema._definition;
  231. if (def.prepare &&
  232. value !== undefined &&
  233. prefs.convert) {
  234. const prepared = def.prepare(value, helpers);
  235. if (prepared) {
  236. state.mainstay.tracer.value(state, 'prepare', value, prepared.value);
  237. if (prepared.errors) {
  238. return internals.finalize(prepared.value, [].concat(prepared.errors), helpers); // Prepare error always aborts early
  239. }
  240. value = prepared.value;
  241. }
  242. }
  243. // Type coercion
  244. if (def.coerce &&
  245. value !== undefined &&
  246. prefs.convert &&
  247. (!def.coerce.from || def.coerce.from.includes(typeof value))) {
  248. const coerced = def.coerce.method(value, helpers);
  249. if (coerced) {
  250. state.mainstay.tracer.value(state, 'coerced', value, coerced.value);
  251. if (coerced.errors) {
  252. return internals.finalize(coerced.value, [].concat(coerced.errors), helpers); // Coerce error always aborts early
  253. }
  254. value = coerced.value;
  255. }
  256. }
  257. // Empty value
  258. const empty = schema._flags.empty;
  259. if (empty &&
  260. empty.$_match(internals.trim(value, schema), state.nest(empty), Common.defaults)) {
  261. state.mainstay.tracer.value(state, 'empty', value, undefined);
  262. value = undefined;
  263. }
  264. // Presence requirements (required, optional, forbidden)
  265. const presence = overrides.presence || schema._flags.presence || (schema._flags._endedSwitch ? null : prefs.presence);
  266. if (value === undefined) {
  267. if (presence === 'forbidden') {
  268. return internals.finalize(value, null, helpers);
  269. }
  270. if (presence === 'required') {
  271. return internals.finalize(value, [schema.$_createError('any.required', value, null, state, prefs)], helpers);
  272. }
  273. if (presence === 'optional') {
  274. if (schema._flags.default !== Common.symbols.deepDefault) {
  275. return internals.finalize(value, null, helpers);
  276. }
  277. state.mainstay.tracer.value(state, 'default', value, {});
  278. value = {};
  279. }
  280. }
  281. else if (presence === 'forbidden') {
  282. return internals.finalize(value, [schema.$_createError('any.unknown', value, null, state, prefs)], helpers);
  283. }
  284. // Allowed values
  285. const errors = [];
  286. if (schema._valids) {
  287. const match = schema._valids.get(value, state, prefs, schema._flags.insensitive);
  288. if (match) {
  289. if (prefs.convert) {
  290. state.mainstay.tracer.value(state, 'valids', value, match.value);
  291. value = match.value;
  292. }
  293. state.mainstay.tracer.filter(schema, state, 'valid', match);
  294. return internals.finalize(value, null, helpers);
  295. }
  296. if (schema._flags.only) {
  297. const report = schema.$_createError('any.only', value, { valids: schema._valids.values({ display: true }) }, state, prefs);
  298. if (prefs.abortEarly) {
  299. return internals.finalize(value, [report], helpers);
  300. }
  301. errors.push(report);
  302. }
  303. }
  304. // Denied values
  305. if (schema._invalids) {
  306. const match = schema._invalids.get(value, state, prefs, schema._flags.insensitive);
  307. if (match) {
  308. state.mainstay.tracer.filter(schema, state, 'invalid', match);
  309. const report = schema.$_createError('any.invalid', value, { invalids: schema._invalids.values({ display: true }) }, state, prefs);
  310. if (prefs.abortEarly) {
  311. return internals.finalize(value, [report], helpers);
  312. }
  313. errors.push(report);
  314. }
  315. }
  316. // Base type
  317. if (def.validate) {
  318. const base = def.validate(value, helpers);
  319. if (base) {
  320. state.mainstay.tracer.value(state, 'base', value, base.value);
  321. value = base.value;
  322. if (base.errors) {
  323. if (!Array.isArray(base.errors)) {
  324. errors.push(base.errors);
  325. return internals.finalize(value, errors, helpers); // Base error always aborts early
  326. }
  327. if (base.errors.length) {
  328. errors.push(...base.errors);
  329. return internals.finalize(value, errors, helpers); // Base error always aborts early
  330. }
  331. }
  332. }
  333. }
  334. // Validate tests
  335. if (!schema._rules.length) {
  336. return internals.finalize(value, errors, helpers);
  337. }
  338. return internals.rules(value, errors, helpers);
  339. };
  340. internals.rules = function (value, errors, helpers) {
  341. const { schema, state, prefs } = helpers;
  342. for (const rule of schema._rules) {
  343. const definition = schema._definition.rules[rule.method];
  344. // Skip rules that are also applied in coerce step
  345. if (definition.convert &&
  346. prefs.convert) {
  347. state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'full');
  348. continue;
  349. }
  350. // Resolve references
  351. let ret;
  352. let args = rule.args;
  353. if (rule._resolve.length) {
  354. args = Object.assign({}, args); // Shallow copy
  355. for (const key of rule._resolve) {
  356. const resolver = definition.argsByName.get(key);
  357. const resolved = args[key].resolve(value, state, prefs);
  358. const normalized = resolver.normalize ? resolver.normalize(resolved) : resolved;
  359. const invalid = Common.validateArg(normalized, null, resolver);
  360. if (invalid) {
  361. ret = schema.$_createError('any.ref', resolved, { arg: key, ref: args[key], reason: invalid }, state, prefs);
  362. break;
  363. }
  364. args[key] = normalized;
  365. }
  366. }
  367. // Test rule
  368. ret = ret || definition.validate(value, helpers, args, rule); // Use ret if already set to reference error
  369. const result = internals.rule(ret, rule);
  370. if (result.errors) {
  371. state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'error');
  372. if (rule.warn) {
  373. state.mainstay.warnings.push(...result.errors);
  374. continue;
  375. }
  376. if (prefs.abortEarly) {
  377. return internals.finalize(value, result.errors, helpers);
  378. }
  379. errors.push(...result.errors);
  380. }
  381. else {
  382. state.mainstay.tracer.log(schema, state, 'rule', rule.name, 'pass');
  383. state.mainstay.tracer.value(state, 'rule', value, result.value, rule.name);
  384. value = result.value;
  385. }
  386. }
  387. return internals.finalize(value, errors, helpers);
  388. };
  389. internals.rule = function (ret, rule) {
  390. if (ret instanceof Errors.Report) {
  391. internals.error(ret, rule);
  392. return { errors: [ret], value: null };
  393. }
  394. if (Array.isArray(ret) &&
  395. ret[Common.symbols.errors]) {
  396. ret.forEach((report) => internals.error(report, rule));
  397. return { errors: ret, value: null };
  398. }
  399. return { errors: null, value: ret };
  400. };
  401. internals.error = function (report, rule) {
  402. if (rule.message) {
  403. report._setTemplate(rule.message);
  404. }
  405. return report;
  406. };
  407. internals.finalize = function (value, errors, helpers) {
  408. errors = errors || [];
  409. const { schema, state, prefs } = helpers;
  410. // Failover value
  411. if (errors.length) {
  412. const failover = internals.default('failover', undefined, errors, helpers);
  413. if (failover !== undefined) {
  414. state.mainstay.tracer.value(state, 'failover', value, failover);
  415. value = failover;
  416. errors = [];
  417. }
  418. }
  419. // Error override
  420. if (errors.length &&
  421. schema._flags.error) {
  422. if (typeof schema._flags.error === 'function') {
  423. errors = schema._flags.error(errors);
  424. if (!Array.isArray(errors)) {
  425. errors = [errors];
  426. }
  427. for (const error of errors) {
  428. Assert(error instanceof Error || error instanceof Errors.Report, 'error() must return an Error object');
  429. }
  430. }
  431. else {
  432. errors = [schema._flags.error];
  433. }
  434. }
  435. // Default
  436. if (value === undefined) {
  437. const defaulted = internals.default('default', value, errors, helpers);
  438. state.mainstay.tracer.value(state, 'default', value, defaulted);
  439. value = defaulted;
  440. }
  441. // Cast
  442. if (schema._flags.cast &&
  443. value !== undefined) {
  444. const caster = schema._definition.cast[schema._flags.cast];
  445. if (caster.from(value)) {
  446. const casted = caster.to(value, helpers);
  447. state.mainstay.tracer.value(state, 'cast', value, casted, schema._flags.cast);
  448. value = casted;
  449. }
  450. }
  451. // Externals
  452. if (schema.$_terms.externals &&
  453. prefs.externals &&
  454. prefs._externals !== false) { // Disabled for matching
  455. for (const { method } of schema.$_terms.externals) {
  456. state.mainstay.externals.push({ method, schema, state, label: Errors.label(schema._flags, state, prefs) });
  457. }
  458. }
  459. // Result
  460. const result = { value, errors: errors.length ? errors : null };
  461. if (schema._flags.result) {
  462. result.value = schema._flags.result === 'strip' ? undefined : /* raw */ helpers.original;
  463. state.mainstay.tracer.value(state, schema._flags.result, value, result.value);
  464. state.shadow(value, schema._flags.result);
  465. }
  466. // Cache
  467. if (schema._cache &&
  468. prefs.cache !== false &&
  469. !schema._refs.length) {
  470. schema._cache.set(helpers.original, result);
  471. }
  472. // Artifacts
  473. if (value !== undefined &&
  474. !result.errors &&
  475. schema._flags.artifact !== undefined) {
  476. state.mainstay.artifacts = state.mainstay.artifacts || new Map();
  477. if (!state.mainstay.artifacts.has(schema._flags.artifact)) {
  478. state.mainstay.artifacts.set(schema._flags.artifact, []);
  479. }
  480. state.mainstay.artifacts.get(schema._flags.artifact).push(state.path);
  481. }
  482. return result;
  483. };
  484. internals.prefs = function (schema, prefs) {
  485. const isDefaultOptions = prefs === Common.defaults;
  486. if (isDefaultOptions &&
  487. schema._preferences[Common.symbols.prefs]) {
  488. return schema._preferences[Common.symbols.prefs];
  489. }
  490. prefs = Common.preferences(prefs, schema._preferences);
  491. if (isDefaultOptions) {
  492. schema._preferences[Common.symbols.prefs] = prefs;
  493. }
  494. return prefs;
  495. };
  496. internals.default = function (flag, value, errors, helpers) {
  497. const { schema, state, prefs } = helpers;
  498. const source = schema._flags[flag];
  499. if (prefs.noDefaults ||
  500. source === undefined) {
  501. return value;
  502. }
  503. state.mainstay.tracer.log(schema, state, 'rule', flag, 'full');
  504. if (!source) {
  505. return source;
  506. }
  507. if (typeof source === 'function') {
  508. const args = source.length ? [Clone(state.ancestors[0]), helpers] : [];
  509. try {
  510. return source(...args);
  511. }
  512. catch (err) {
  513. errors.push(schema.$_createError(`any.${flag}`, null, { error: err }, state, prefs));
  514. return;
  515. }
  516. }
  517. if (typeof source !== 'object') {
  518. return source;
  519. }
  520. if (source[Common.symbols.literal]) {
  521. return source.literal;
  522. }
  523. if (Common.isResolvable(source)) {
  524. return source.resolve(value, state, prefs);
  525. }
  526. return Clone(source);
  527. };
  528. internals.trim = function (value, schema) {
  529. if (typeof value !== 'string') {
  530. return value;
  531. }
  532. const trim = schema.$_getRule('trim');
  533. if (!trim ||
  534. !trim.args.enabled) {
  535. return value;
  536. }
  537. return value.trim();
  538. };
  539. internals.ignore = {
  540. active: false,
  541. debug: Ignore,
  542. entry: Ignore,
  543. filter: Ignore,
  544. log: Ignore,
  545. resolve: Ignore,
  546. value: Ignore
  547. };
  548. internals.errorsArray = function () {
  549. const errors = [];
  550. errors[Common.symbols.errors] = true;
  551. return errors;
  552. };