index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. 'use strict';
  2. // Load modules
  3. const Hoek = require('hoek');
  4. const Any = require('./types/any');
  5. const Cast = require('./cast');
  6. const Errors = require('./errors');
  7. const Lazy = require('./types/lazy');
  8. const Ref = require('./ref');
  9. const Settings = require('./types/any/settings');
  10. // Declare internals
  11. const internals = {
  12. alternatives: require('./types/alternatives'),
  13. array: require('./types/array'),
  14. boolean: require('./types/boolean'),
  15. binary: require('./types/binary'),
  16. date: require('./types/date'),
  17. func: require('./types/func'),
  18. number: require('./types/number'),
  19. object: require('./types/object'),
  20. string: require('./types/string'),
  21. symbol: require('./types/symbol')
  22. };
  23. internals.callWithDefaults = function (schema, args) {
  24. Hoek.assert(this, 'Must be invoked on a Joi instance.');
  25. if (this._defaults) {
  26. schema = this._defaults(schema);
  27. }
  28. schema._currentJoi = this;
  29. return schema._init(...args);
  30. };
  31. internals.root = function () {
  32. const any = new Any();
  33. const root = any.clone();
  34. Any.prototype._currentJoi = root;
  35. root._currentJoi = root;
  36. root._binds = new Set(['any', 'alternatives', 'alt', 'array', 'boolean', 'binary', 'date', 'func', 'number', 'object', 'string', 'symbol', 'validate', 'describe', 'compile', 'assert', 'attempt', 'lazy', 'defaults', 'extend', 'allow', 'valid', 'only', 'equal', 'invalid', 'disallow', 'not', 'required', 'exist', 'optional', 'forbidden', 'strip', 'when', 'empty', 'default']);
  37. root.any = function (...args) {
  38. Hoek.assert(args.length === 0, 'Joi.any() does not allow arguments.');
  39. return internals.callWithDefaults.call(this, any, args);
  40. };
  41. root.alternatives = root.alt = function (...args) {
  42. return internals.callWithDefaults.call(this, internals.alternatives, args);
  43. };
  44. root.array = function (...args) {
  45. Hoek.assert(args.length === 0, 'Joi.array() does not allow arguments.');
  46. return internals.callWithDefaults.call(this, internals.array, args);
  47. };
  48. root.boolean = root.bool = function (...args) {
  49. Hoek.assert(args.length === 0, 'Joi.boolean() does not allow arguments.');
  50. return internals.callWithDefaults.call(this, internals.boolean, args);
  51. };
  52. root.binary = function (...args) {
  53. Hoek.assert(args.length === 0, 'Joi.binary() does not allow arguments.');
  54. return internals.callWithDefaults.call(this, internals.binary, args);
  55. };
  56. root.date = function (...args) {
  57. Hoek.assert(args.length === 0, 'Joi.date() does not allow arguments.');
  58. return internals.callWithDefaults.call(this, internals.date, args);
  59. };
  60. root.func = function (...args) {
  61. Hoek.assert(args.length === 0, 'Joi.func() does not allow arguments.');
  62. return internals.callWithDefaults.call(this, internals.func, args);
  63. };
  64. root.number = function (...args) {
  65. Hoek.assert(args.length === 0, 'Joi.number() does not allow arguments.');
  66. return internals.callWithDefaults.call(this, internals.number, args);
  67. };
  68. root.object = function (...args) {
  69. return internals.callWithDefaults.call(this, internals.object, args);
  70. };
  71. root.string = function (...args) {
  72. Hoek.assert(args.length === 0, 'Joi.string() does not allow arguments.');
  73. return internals.callWithDefaults.call(this, internals.string, args);
  74. };
  75. root.symbol = function (...args) {
  76. Hoek.assert(args.length === 0, 'Joi.symbol() does not allow arguments.');
  77. return internals.callWithDefaults.call(this, internals.symbol, args);
  78. };
  79. root.ref = function (...args) {
  80. return Ref.create(...args);
  81. };
  82. root.isRef = function (ref) {
  83. return Ref.isRef(ref);
  84. };
  85. root.validate = function (value, ...args /*, [schema], [options], callback */) {
  86. const last = args[args.length - 1];
  87. const callback = typeof last === 'function' ? last : null;
  88. const count = args.length - (callback ? 1 : 0);
  89. if (count === 0) {
  90. return any.validate(value, callback);
  91. }
  92. const options = count === 2 ? args[1] : undefined;
  93. const schema = this.compile(args[0]);
  94. return schema._validateWithOptions(value, options, callback);
  95. };
  96. root.describe = function (...args) {
  97. const schema = args.length ? this.compile(args[0]) : any;
  98. return schema.describe();
  99. };
  100. root.compile = function (schema) {
  101. try {
  102. return Cast.schema(this, schema);
  103. }
  104. catch (err) {
  105. if (err.hasOwnProperty('path')) {
  106. err.message = err.message + '(' + err.path + ')';
  107. }
  108. throw err;
  109. }
  110. };
  111. root.assert = function (value, schema, message) {
  112. this.attempt(value, schema, message);
  113. };
  114. root.attempt = function (value, schema, message) {
  115. const result = this.validate(value, schema);
  116. const error = result.error;
  117. if (error) {
  118. if (!message) {
  119. if (typeof error.annotate === 'function') {
  120. error.message = error.annotate();
  121. }
  122. throw error;
  123. }
  124. if (!(message instanceof Error)) {
  125. if (typeof error.annotate === 'function') {
  126. error.message = `${message} ${error.annotate()}`;
  127. }
  128. throw error;
  129. }
  130. throw message;
  131. }
  132. return result.value;
  133. };
  134. root.reach = function (schema, path) {
  135. Hoek.assert(schema && schema instanceof Any, 'you must provide a joi schema');
  136. Hoek.assert(Array.isArray(path) || typeof path === 'string', 'path must be a string or an array of strings');
  137. const reach = (sourceSchema, schemaPath) => {
  138. if (!schemaPath.length) {
  139. return sourceSchema;
  140. }
  141. const children = sourceSchema._inner.children;
  142. if (!children) {
  143. return;
  144. }
  145. const key = schemaPath.shift();
  146. for (let i = 0; i < children.length; ++i) {
  147. const child = children[i];
  148. if (child.key === key) {
  149. return reach(child.schema, schemaPath);
  150. }
  151. }
  152. };
  153. const schemaPath = typeof path === 'string' ? (path ? path.split('.') : []) : path.slice();
  154. return reach(schema, schemaPath);
  155. };
  156. root.lazy = function (...args) {
  157. return internals.callWithDefaults.call(this, Lazy, args);
  158. };
  159. root.defaults = function (fn) {
  160. Hoek.assert(typeof fn === 'function', 'Defaults must be a function');
  161. let joi = Object.create(this.any());
  162. joi = fn(joi);
  163. Hoek.assert(joi && joi instanceof this.constructor, 'defaults() must return a schema');
  164. Object.assign(joi, this, joi.clone()); // Re-add the types from `this` but also keep the settings from joi's potential new defaults
  165. joi._defaults = (schema) => {
  166. if (this._defaults) {
  167. schema = this._defaults(schema);
  168. Hoek.assert(schema instanceof this.constructor, 'defaults() must return a schema');
  169. }
  170. schema = fn(schema);
  171. Hoek.assert(schema instanceof this.constructor, 'defaults() must return a schema');
  172. return schema;
  173. };
  174. return joi;
  175. };
  176. root.bind = function () {
  177. const joi = Object.create(this);
  178. joi._binds.forEach((bind) => {
  179. joi[bind] = joi[bind].bind(joi);
  180. });
  181. return joi;
  182. };
  183. root.extend = function (...args) {
  184. const extensions = Hoek.flatten(args);
  185. Hoek.assert(extensions.length > 0, 'You need to provide at least one extension');
  186. this.assert(extensions, root.extensionsSchema);
  187. const joi = Object.create(this.any());
  188. Object.assign(joi, this);
  189. joi._currentJoi = joi;
  190. joi._binds = new Set(joi._binds);
  191. for (let i = 0; i < extensions.length; ++i) {
  192. let extension = extensions[i];
  193. if (typeof extension === 'function') {
  194. extension = extension(joi);
  195. }
  196. this.assert(extension, root.extensionSchema);
  197. const base = (extension.base || this.any()).clone(); // Cloning because we're going to override language afterwards
  198. const ctor = base.constructor;
  199. const type = class extends ctor { // eslint-disable-line no-loop-func
  200. constructor() {
  201. super();
  202. if (extension.base) {
  203. Object.assign(this, base);
  204. }
  205. this._type = extension.name;
  206. if (extension.language) {
  207. this._settings = Settings.concat(this._settings, {
  208. language: {
  209. [extension.name]: extension.language
  210. }
  211. });
  212. }
  213. }
  214. };
  215. if (extension.coerce) {
  216. type.prototype._coerce = function (value, state, options) {
  217. if (ctor.prototype._coerce) {
  218. const baseRet = ctor.prototype._coerce.call(this, value, state, options);
  219. if (baseRet.errors) {
  220. return baseRet;
  221. }
  222. value = baseRet.value;
  223. }
  224. const ret = extension.coerce.call(this, value, state, options);
  225. if (ret instanceof Errors.Err) {
  226. return { value, errors: ret };
  227. }
  228. return { value: ret };
  229. };
  230. }
  231. if (extension.pre) {
  232. type.prototype._base = function (value, state, options) {
  233. if (ctor.prototype._base) {
  234. const baseRet = ctor.prototype._base.call(this, value, state, options);
  235. if (baseRet.errors) {
  236. return baseRet;
  237. }
  238. value = baseRet.value;
  239. }
  240. const ret = extension.pre.call(this, value, state, options);
  241. if (ret instanceof Errors.Err) {
  242. return { value, errors: ret };
  243. }
  244. return { value: ret };
  245. };
  246. }
  247. if (extension.rules) {
  248. for (let j = 0; j < extension.rules.length; ++j) {
  249. const rule = extension.rules[j];
  250. const ruleArgs = rule.params ?
  251. (rule.params instanceof Any ? rule.params._inner.children.map((k) => k.key) : Object.keys(rule.params)) :
  252. [];
  253. const validateArgs = rule.params ? Cast.schema(this, rule.params) : null;
  254. type.prototype[rule.name] = function (...rArgs) { // eslint-disable-line no-loop-func
  255. if (rArgs.length > ruleArgs.length) {
  256. throw new Error('Unexpected number of arguments');
  257. }
  258. let hasRef = false;
  259. let arg = {};
  260. for (let k = 0; k < ruleArgs.length; ++k) {
  261. arg[ruleArgs[k]] = rArgs[k];
  262. if (!hasRef && Ref.isRef(rArgs[k])) {
  263. hasRef = true;
  264. }
  265. }
  266. if (validateArgs) {
  267. arg = joi.attempt(arg, validateArgs);
  268. }
  269. let schema;
  270. if (rule.validate) {
  271. const validate = function (value, state, options) {
  272. return rule.validate.call(this, arg, value, state, options);
  273. };
  274. schema = this._test(rule.name, arg, validate, {
  275. description: rule.description,
  276. hasRef
  277. });
  278. }
  279. else {
  280. schema = this.clone();
  281. }
  282. if (rule.setup) {
  283. const newSchema = rule.setup.call(schema, arg);
  284. if (newSchema !== undefined) {
  285. Hoek.assert(newSchema instanceof Any, `Setup of extension Joi.${this._type}().${rule.name}() must return undefined or a Joi object`);
  286. schema = newSchema;
  287. }
  288. }
  289. return schema;
  290. };
  291. }
  292. }
  293. if (extension.describe) {
  294. type.prototype.describe = function () {
  295. const description = ctor.prototype.describe.call(this);
  296. return extension.describe.call(this, description);
  297. };
  298. }
  299. const instance = new type();
  300. joi[extension.name] = function (...extArgs) {
  301. return internals.callWithDefaults.call(this, instance, extArgs);
  302. };
  303. joi._binds.add(extension.name);
  304. }
  305. return joi;
  306. };
  307. root.extensionSchema = internals.object.keys({
  308. base: internals.object.type(Any, 'Joi object'),
  309. name: internals.string.required(),
  310. coerce: internals.func.arity(3),
  311. pre: internals.func.arity(3),
  312. language: internals.object,
  313. describe: internals.func.arity(1),
  314. rules: internals.array.items(internals.object.keys({
  315. name: internals.string.required(),
  316. setup: internals.func.arity(1),
  317. validate: internals.func.arity(4),
  318. params: [
  319. internals.object.pattern(/.*/, internals.object.type(Any, 'Joi object')),
  320. internals.object.type(internals.object.constructor, 'Joi object')
  321. ],
  322. description: [internals.string, internals.func.arity(1)]
  323. }).or('setup', 'validate'))
  324. }).strict();
  325. root.extensionsSchema = internals.array.items([internals.object, internals.func.arity(1)]).strict();
  326. root.version = require('../package.json').version;
  327. return root;
  328. };
  329. module.exports = internals.root();