object.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. 'use strict';
  2. // Load modules
  3. const Hoek = require('hoek');
  4. const Topo = require('topo');
  5. const Any = require('./any');
  6. const Errors = require('./errors');
  7. const Cast = require('./cast');
  8. const Ref = require('./ref');
  9. // Declare internals
  10. const internals = {};
  11. internals.Object = class extends Any {
  12. constructor() {
  13. super();
  14. this._type = 'object';
  15. this._inner.children = null;
  16. this._inner.renames = [];
  17. this._inner.dependencies = [];
  18. this._inner.patterns = [];
  19. }
  20. _base(value, state, options) {
  21. let target = value;
  22. const errors = [];
  23. const finish = () => {
  24. return {
  25. value: target,
  26. errors: errors.length ? errors : null
  27. };
  28. };
  29. if (typeof value === 'string' &&
  30. options.convert) {
  31. value = internals.safeParse(value);
  32. }
  33. const type = this._flags.func ? 'function' : 'object';
  34. if (!value ||
  35. typeof value !== type ||
  36. Array.isArray(value)) {
  37. errors.push(this.createError(type + '.base', null, state, options));
  38. return finish();
  39. }
  40. // Skip if there are no other rules to test
  41. if (!this._inner.renames.length &&
  42. !this._inner.dependencies.length &&
  43. !this._inner.children && // null allows any keys
  44. !this._inner.patterns.length) {
  45. target = value;
  46. return finish();
  47. }
  48. // Ensure target is a local copy (parsed) or shallow copy
  49. if (target === value) {
  50. if (type === 'object') {
  51. target = Object.create(Object.getPrototypeOf(value));
  52. }
  53. else {
  54. target = function () {
  55. return value.apply(this, arguments);
  56. };
  57. target.prototype = Hoek.clone(value.prototype);
  58. }
  59. const valueKeys = Object.keys(value);
  60. for (let i = 0; i < valueKeys.length; ++i) {
  61. target[valueKeys[i]] = value[valueKeys[i]];
  62. }
  63. }
  64. else {
  65. target = value;
  66. }
  67. // Rename keys
  68. const renamed = {};
  69. for (let i = 0; i < this._inner.renames.length; ++i) {
  70. const rename = this._inner.renames[i];
  71. if (rename.options.ignoreUndefined && target[rename.from] === undefined) {
  72. continue;
  73. }
  74. if (!rename.options.multiple &&
  75. renamed[rename.to]) {
  76. errors.push(this.createError('object.rename.multiple', { from: rename.from, to: rename.to }, state, options));
  77. if (options.abortEarly) {
  78. return finish();
  79. }
  80. }
  81. if (Object.prototype.hasOwnProperty.call(target, rename.to) &&
  82. !rename.options.override &&
  83. !renamed[rename.to]) {
  84. errors.push(this.createError('object.rename.override', { from: rename.from, to: rename.to }, state, options));
  85. if (options.abortEarly) {
  86. return finish();
  87. }
  88. }
  89. if (target[rename.from] === undefined) {
  90. delete target[rename.to];
  91. }
  92. else {
  93. target[rename.to] = target[rename.from];
  94. }
  95. renamed[rename.to] = true;
  96. if (!rename.options.alias) {
  97. delete target[rename.from];
  98. }
  99. }
  100. // Validate schema
  101. if (!this._inner.children && // null allows any keys
  102. !this._inner.patterns.length &&
  103. !this._inner.dependencies.length) {
  104. return finish();
  105. }
  106. const unprocessed = Hoek.mapToObject(Object.keys(target));
  107. if (this._inner.children) {
  108. for (let i = 0; i < this._inner.children.length; ++i) {
  109. const child = this._inner.children[i];
  110. const key = child.key;
  111. const item = target[key];
  112. delete unprocessed[key];
  113. const localState = { key, path: (state.path || '') + (state.path && key ? '.' : '') + key, parent: target, reference: state.reference };
  114. const result = child.schema._validate(item, localState, options);
  115. if (result.errors) {
  116. errors.push(this.createError('object.child', { key, child: child.schema._getLabel(key), reason: result.errors }, localState, options));
  117. if (options.abortEarly) {
  118. return finish();
  119. }
  120. }
  121. if (child.schema._flags.strip || (result.value === undefined && result.value !== item)) {
  122. delete target[key];
  123. }
  124. else if (result.value !== undefined) {
  125. target[key] = result.value;
  126. }
  127. }
  128. }
  129. // Unknown keys
  130. let unprocessedKeys = Object.keys(unprocessed);
  131. if (unprocessedKeys.length &&
  132. this._inner.patterns.length) {
  133. for (let i = 0; i < unprocessedKeys.length; ++i) {
  134. const key = unprocessedKeys[i];
  135. const localState = { key, path: (state.path ? state.path + '.' : '') + key, parent: target, reference: state.reference };
  136. const item = target[key];
  137. for (let j = 0; j < this._inner.patterns.length; ++j) {
  138. const pattern = this._inner.patterns[j];
  139. if (pattern.regex.test(key)) {
  140. delete unprocessed[key];
  141. const result = pattern.rule._validate(item, localState, options);
  142. if (result.errors) {
  143. errors.push(this.createError('object.child', { key, child: pattern.rule._getLabel(key), reason: result.errors }, localState, options));
  144. if (options.abortEarly) {
  145. return finish();
  146. }
  147. }
  148. if (result.value !== undefined) {
  149. target[key] = result.value;
  150. }
  151. }
  152. }
  153. }
  154. unprocessedKeys = Object.keys(unprocessed);
  155. }
  156. if ((this._inner.children || this._inner.patterns.length) && unprocessedKeys.length) {
  157. if (options.stripUnknown ||
  158. options.skipFunctions) {
  159. const stripUnknown = options.stripUnknown
  160. ? (options.stripUnknown === true ? true : !!options.stripUnknown.objects)
  161. : false;
  162. for (let i = 0; i < unprocessedKeys.length; ++i) {
  163. const key = unprocessedKeys[i];
  164. if (stripUnknown) {
  165. delete target[key];
  166. delete unprocessed[key];
  167. }
  168. else if (typeof target[key] === 'function') {
  169. delete unprocessed[key];
  170. }
  171. }
  172. unprocessedKeys = Object.keys(unprocessed);
  173. }
  174. if (unprocessedKeys.length &&
  175. (this._flags.allowUnknown !== undefined ? !this._flags.allowUnknown : !options.allowUnknown)) {
  176. for (let i = 0; i < unprocessedKeys.length; ++i) {
  177. const unprocessedKey = unprocessedKeys[i];
  178. errors.push(this.createError('object.allowUnknown', { child: unprocessedKey }, { key: unprocessedKey, path: state.path + (state.path ? '.' : '') + unprocessedKey }, options));
  179. }
  180. }
  181. }
  182. // Validate dependencies
  183. for (let i = 0; i < this._inner.dependencies.length; ++i) {
  184. const dep = this._inner.dependencies[i];
  185. const err = internals[dep.type].call(this, dep.key !== null && target[dep.key], dep.peers, target, { key: dep.key, path: (state.path || '') + (dep.key ? '.' + dep.key : '') }, options);
  186. if (err instanceof Errors.Err) {
  187. errors.push(err);
  188. if (options.abortEarly) {
  189. return finish();
  190. }
  191. }
  192. }
  193. return finish();
  194. }
  195. _func() {
  196. const obj = this.clone();
  197. obj._flags.func = true;
  198. return obj;
  199. }
  200. keys(schema) {
  201. Hoek.assert(schema === null || schema === undefined || typeof schema === 'object', 'Object schema must be a valid object');
  202. Hoek.assert(!schema || !(schema instanceof Any), 'Object schema cannot be a joi schema');
  203. const obj = this.clone();
  204. if (!schema) {
  205. obj._inner.children = null;
  206. return obj;
  207. }
  208. const children = Object.keys(schema);
  209. if (!children.length) {
  210. obj._inner.children = [];
  211. return obj;
  212. }
  213. const topo = new Topo();
  214. if (obj._inner.children) {
  215. for (let i = 0; i < obj._inner.children.length; ++i) {
  216. const child = obj._inner.children[i];
  217. // Only add the key if we are not going to replace it later
  218. if (children.indexOf(child.key) === -1) {
  219. topo.add(child, { after: child._refs, group: child.key });
  220. }
  221. }
  222. }
  223. for (let i = 0; i < children.length; ++i) {
  224. const key = children[i];
  225. const child = schema[key];
  226. try {
  227. const cast = Cast.schema(child);
  228. topo.add({ key, schema: cast }, { after: cast._refs, group: key });
  229. }
  230. catch (castErr) {
  231. if (castErr.hasOwnProperty('path')) {
  232. castErr.path = key + '.' + castErr.path;
  233. }
  234. else {
  235. castErr.path = key;
  236. }
  237. throw castErr;
  238. }
  239. }
  240. obj._inner.children = topo.nodes;
  241. return obj;
  242. }
  243. unknown(allow) {
  244. const obj = this.clone();
  245. obj._flags.allowUnknown = (allow !== false);
  246. return obj;
  247. }
  248. length(limit) {
  249. Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');
  250. return this._test('length', limit, function (value, state, options) {
  251. if (Object.keys(value).length === limit) {
  252. return value;
  253. }
  254. return this.createError('object.length', { limit }, state, options);
  255. });
  256. }
  257. arity(n) {
  258. Hoek.assert(Hoek.isInteger(n) && n >= 0, 'n must be a positive integer');
  259. return this._test('arity', n, function (value, state, options) {
  260. if (value.length === n) {
  261. return value;
  262. }
  263. return this.createError('function.arity', { n }, state, options);
  264. });
  265. }
  266. minArity(n) {
  267. Hoek.assert(Hoek.isInteger(n) && n > 0, 'n must be a strict positive integer');
  268. return this._test('minArity', n, function (value, state, options) {
  269. if (value.length >= n) {
  270. return value;
  271. }
  272. return this.createError('function.minArity', { n }, state, options);
  273. });
  274. }
  275. maxArity(n) {
  276. Hoek.assert(Hoek.isInteger(n) && n >= 0, 'n must be a positive integer');
  277. return this._test('maxArity', n, function (value, state, options) {
  278. if (value.length <= n) {
  279. return value;
  280. }
  281. return this.createError('function.maxArity', { n }, state, options);
  282. });
  283. }
  284. min(limit) {
  285. Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');
  286. return this._test('min', limit, function (value, state, options) {
  287. if (Object.keys(value).length >= limit) {
  288. return value;
  289. }
  290. return this.createError('object.min', { limit }, state, options);
  291. });
  292. }
  293. max(limit) {
  294. Hoek.assert(Hoek.isInteger(limit) && limit >= 0, 'limit must be a positive integer');
  295. return this._test('max', limit, function (value, state, options) {
  296. if (Object.keys(value).length <= limit) {
  297. return value;
  298. }
  299. return this.createError('object.max', { limit }, state, options);
  300. });
  301. }
  302. pattern(pattern, schema) {
  303. Hoek.assert(pattern instanceof RegExp, 'Invalid regular expression');
  304. Hoek.assert(schema !== undefined, 'Invalid rule');
  305. pattern = new RegExp(pattern.source, pattern.ignoreCase ? 'i' : undefined); // Future version should break this and forbid unsupported regex flags
  306. try {
  307. schema = Cast.schema(schema);
  308. }
  309. catch (castErr) {
  310. if (castErr.hasOwnProperty('path')) {
  311. castErr.message = castErr.message + '(' + castErr.path + ')';
  312. }
  313. throw castErr;
  314. }
  315. const obj = this.clone();
  316. obj._inner.patterns.push({ regex: pattern, rule: schema });
  317. return obj;
  318. }
  319. schema() {
  320. return this._test('schema', null, function (value, state, options) {
  321. if (value instanceof Any) {
  322. return value;
  323. }
  324. return this.createError('object.schema', null, state, options);
  325. });
  326. }
  327. with(key, peers) {
  328. return this._dependency('with', key, peers);
  329. }
  330. without(key, peers) {
  331. return this._dependency('without', key, peers);
  332. }
  333. xor() {
  334. const peers = Hoek.flatten(Array.prototype.slice.call(arguments));
  335. return this._dependency('xor', null, peers);
  336. }
  337. or() {
  338. const peers = Hoek.flatten(Array.prototype.slice.call(arguments));
  339. return this._dependency('or', null, peers);
  340. }
  341. and() {
  342. const peers = Hoek.flatten(Array.prototype.slice.call(arguments));
  343. return this._dependency('and', null, peers);
  344. }
  345. nand() {
  346. const peers = Hoek.flatten(Array.prototype.slice.call(arguments));
  347. return this._dependency('nand', null, peers);
  348. }
  349. requiredKeys(children) {
  350. children = Hoek.flatten(Array.prototype.slice.call(arguments));
  351. return this.applyFunctionToChildren(children, 'required');
  352. }
  353. optionalKeys(children) {
  354. children = Hoek.flatten(Array.prototype.slice.call(arguments));
  355. return this.applyFunctionToChildren(children, 'optional');
  356. }
  357. rename(from, to, options) {
  358. Hoek.assert(typeof from === 'string', 'Rename missing the from argument');
  359. Hoek.assert(typeof to === 'string', 'Rename missing the to argument');
  360. Hoek.assert(to !== from, 'Cannot rename key to same name:', from);
  361. for (let i = 0; i < this._inner.renames.length; ++i) {
  362. Hoek.assert(this._inner.renames[i].from !== from, 'Cannot rename the same key multiple times');
  363. }
  364. const obj = this.clone();
  365. obj._inner.renames.push({
  366. from,
  367. to,
  368. options: Hoek.applyToDefaults(internals.renameDefaults, options || {})
  369. });
  370. return obj;
  371. }
  372. applyFunctionToChildren(children, fn, args, root) {
  373. children = [].concat(children);
  374. Hoek.assert(children.length > 0, 'expected at least one children');
  375. const groupedChildren = internals.groupChildren(children);
  376. let obj;
  377. if ('' in groupedChildren) {
  378. obj = this[fn].apply(this, args);
  379. delete groupedChildren[''];
  380. }
  381. else {
  382. obj = this.clone();
  383. }
  384. if (obj._inner.children) {
  385. root = root ? (root + '.') : '';
  386. for (let i = 0; i < obj._inner.children.length; ++i) {
  387. const child = obj._inner.children[i];
  388. const group = groupedChildren[child.key];
  389. if (group) {
  390. obj._inner.children[i] = {
  391. key: child.key,
  392. _refs: child._refs,
  393. schema: child.schema.applyFunctionToChildren(group, fn, args, root + child.key)
  394. };
  395. delete groupedChildren[child.key];
  396. }
  397. }
  398. }
  399. const remaining = Object.keys(groupedChildren);
  400. Hoek.assert(remaining.length === 0, 'unknown key(s)', remaining.join(', '));
  401. return obj;
  402. }
  403. _dependency(type, key, peers) {
  404. peers = [].concat(peers);
  405. for (let i = 0; i < peers.length; ++i) {
  406. Hoek.assert(typeof peers[i] === 'string', type, 'peers must be a string or array of strings');
  407. }
  408. const obj = this.clone();
  409. obj._inner.dependencies.push({ type, key, peers });
  410. return obj;
  411. }
  412. describe(shallow) {
  413. const description = Any.prototype.describe.call(this);
  414. if (description.rules) {
  415. for (let i = 0; i < description.rules.length; ++i) {
  416. const rule = description.rules[i];
  417. // Coverage off for future-proof descriptions, only object().assert() is use right now
  418. if (/* $lab:coverage:off$ */rule.arg &&
  419. typeof rule.arg === 'object' &&
  420. rule.arg.schema &&
  421. rule.arg.ref /* $lab:coverage:on$ */) {
  422. rule.arg = {
  423. schema: rule.arg.schema.describe(),
  424. ref: rule.arg.ref.toString()
  425. };
  426. }
  427. }
  428. }
  429. if (this._inner.children &&
  430. !shallow) {
  431. description.children = {};
  432. for (let i = 0; i < this._inner.children.length; ++i) {
  433. const child = this._inner.children[i];
  434. description.children[child.key] = child.schema.describe();
  435. }
  436. }
  437. if (this._inner.dependencies.length) {
  438. description.dependencies = Hoek.clone(this._inner.dependencies);
  439. }
  440. if (this._inner.patterns.length) {
  441. description.patterns = [];
  442. for (let i = 0; i < this._inner.patterns.length; ++i) {
  443. const pattern = this._inner.patterns[i];
  444. description.patterns.push({ regex: pattern.regex.toString(), rule: pattern.rule.describe() });
  445. }
  446. }
  447. return description;
  448. }
  449. assert(ref, schema, message) {
  450. ref = Cast.ref(ref);
  451. Hoek.assert(ref.isContext || ref.depth > 1, 'Cannot use assertions for root level references - use direct key rules instead');
  452. message = message || 'pass the assertion test';
  453. try {
  454. schema = Cast.schema(schema);
  455. }
  456. catch (castErr) {
  457. if (castErr.hasOwnProperty('path')) {
  458. castErr.message = castErr.message + '(' + castErr.path + ')';
  459. }
  460. throw castErr;
  461. }
  462. const key = ref.path[ref.path.length - 1];
  463. const path = ref.path.join('.');
  464. return this._test('assert', { schema, ref }, function (value, state, options) {
  465. const result = schema._validate(ref(value), null, options, value);
  466. if (!result.errors) {
  467. return value;
  468. }
  469. const localState = Hoek.merge({}, state);
  470. localState.key = key;
  471. localState.path = path;
  472. return this.createError('object.assert', { ref: localState.path, message }, localState, options);
  473. });
  474. }
  475. type(constructor, name) {
  476. Hoek.assert(typeof constructor === 'function', 'type must be a constructor function');
  477. name = name || constructor.name;
  478. return this._test('type', name, function (value, state, options) {
  479. if (value instanceof constructor) {
  480. return value;
  481. }
  482. return this.createError('object.type', { type: name }, state, options);
  483. });
  484. }
  485. ref() {
  486. return this._test('ref', null, function (value, state, options) {
  487. if (Ref.isRef(value)) {
  488. return value;
  489. }
  490. return this.createError('function.ref', null, state, options);
  491. });
  492. }
  493. };
  494. internals.safeParse = function (value) {
  495. try {
  496. return JSON.parse(value);
  497. }
  498. catch (parseErr) {}
  499. return value;
  500. };
  501. internals.renameDefaults = {
  502. alias: false, // Keep old value in place
  503. multiple: false, // Allow renaming multiple keys into the same target
  504. override: false // Overrides an existing key
  505. };
  506. internals.groupChildren = function (children) {
  507. children.sort();
  508. const grouped = {};
  509. for (let i = 0; i < children.length; ++i) {
  510. const child = children[i];
  511. Hoek.assert(typeof child === 'string', 'children must be strings');
  512. const group = child.split('.')[0];
  513. const childGroup = grouped[group] = (grouped[group] || []);
  514. childGroup.push(child.substring(group.length + 1));
  515. }
  516. return grouped;
  517. };
  518. internals.with = function (value, peers, parent, state, options) {
  519. if (value === undefined) {
  520. return value;
  521. }
  522. for (let i = 0; i < peers.length; ++i) {
  523. const peer = peers[i];
  524. if (!Object.prototype.hasOwnProperty.call(parent, peer) ||
  525. parent[peer] === undefined) {
  526. return this.createError('object.with', { peer }, state, options);
  527. }
  528. }
  529. return value;
  530. };
  531. internals.without = function (value, peers, parent, state, options) {
  532. if (value === undefined) {
  533. return value;
  534. }
  535. for (let i = 0; i < peers.length; ++i) {
  536. const peer = peers[i];
  537. if (Object.prototype.hasOwnProperty.call(parent, peer) &&
  538. parent[peer] !== undefined) {
  539. return this.createError('object.without', { peer }, state, options);
  540. }
  541. }
  542. return value;
  543. };
  544. internals.xor = function (value, peers, parent, state, options) {
  545. const present = [];
  546. for (let i = 0; i < peers.length; ++i) {
  547. const peer = peers[i];
  548. if (Object.prototype.hasOwnProperty.call(parent, peer) &&
  549. parent[peer] !== undefined) {
  550. present.push(peer);
  551. }
  552. }
  553. if (present.length === 1) {
  554. return value;
  555. }
  556. if (present.length === 0) {
  557. return this.createError('object.missing', { peers }, state, options);
  558. }
  559. return this.createError('object.xor', { peers }, state, options);
  560. };
  561. internals.or = function (value, peers, parent, state, options) {
  562. for (let i = 0; i < peers.length; ++i) {
  563. const peer = peers[i];
  564. if (Object.prototype.hasOwnProperty.call(parent, peer) &&
  565. parent[peer] !== undefined) {
  566. return value;
  567. }
  568. }
  569. return this.createError('object.missing', { peers }, state, options);
  570. };
  571. internals.and = function (value, peers, parent, state, options) {
  572. const missing = [];
  573. const present = [];
  574. const count = peers.length;
  575. for (let i = 0; i < count; ++i) {
  576. const peer = peers[i];
  577. if (!Object.prototype.hasOwnProperty.call(parent, peer) ||
  578. parent[peer] === undefined) {
  579. missing.push(peer);
  580. }
  581. else {
  582. present.push(peer);
  583. }
  584. }
  585. const aon = (missing.length === count || present.length === count);
  586. return !aon ? this.createError('object.and', { present, missing }, state, options) : null;
  587. };
  588. internals.nand = function (value, peers, parent, state, options) {
  589. const present = [];
  590. for (let i = 0; i < peers.length; ++i) {
  591. const peer = peers[i];
  592. if (Object.prototype.hasOwnProperty.call(parent, peer) &&
  593. parent[peer] !== undefined) {
  594. present.push(peer);
  595. }
  596. }
  597. const values = Hoek.clone(peers);
  598. const main = values.splice(0, 1)[0];
  599. const allPresent = (present.length === peers.length);
  600. return allPresent ? this.createError('object.nand', { main, peers: values }, state, options) : null;
  601. };
  602. module.exports = new internals.Object();