compile.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const Common = require('./common');
  4. const Ref = require('./ref');
  5. const internals = {};
  6. exports.schema = function (Joi, config, options = {}) {
  7. Common.assertOptions(options, ['appendPath', 'override']);
  8. try {
  9. return internals.schema(Joi, config, options);
  10. }
  11. catch (err) {
  12. if (options.appendPath &&
  13. err.path !== undefined) {
  14. err.message = `${err.message} (${err.path})`;
  15. }
  16. throw err;
  17. }
  18. };
  19. internals.schema = function (Joi, config, options) {
  20. Assert(config !== undefined, 'Invalid undefined schema');
  21. if (Array.isArray(config)) {
  22. Assert(config.length, 'Invalid empty array schema');
  23. if (config.length === 1) {
  24. config = config[0];
  25. }
  26. }
  27. const valid = (base, ...values) => {
  28. if (options.override !== false) {
  29. return base.valid(Joi.override, ...values);
  30. }
  31. return base.valid(...values);
  32. };
  33. if (internals.simple(config)) {
  34. return valid(Joi, config);
  35. }
  36. if (typeof config === 'function') {
  37. return Joi.custom(config);
  38. }
  39. Assert(typeof config === 'object', 'Invalid schema content:', typeof config);
  40. if (Common.isResolvable(config)) {
  41. return valid(Joi, config);
  42. }
  43. if (Common.isSchema(config)) {
  44. return config;
  45. }
  46. if (Array.isArray(config)) {
  47. for (const item of config) {
  48. if (!internals.simple(item)) {
  49. return Joi.alternatives().try(...config);
  50. }
  51. }
  52. return valid(Joi, ...config);
  53. }
  54. if (config instanceof RegExp) {
  55. return Joi.string().regex(config);
  56. }
  57. if (config instanceof Date) {
  58. return valid(Joi.date(), config);
  59. }
  60. Assert(Object.getPrototypeOf(config) === Object.getPrototypeOf({}), 'Schema can only contain plain objects');
  61. return Joi.object().keys(config);
  62. };
  63. exports.ref = function (id, options) {
  64. return Ref.isRef(id) ? id : Ref.create(id, options);
  65. };
  66. exports.compile = function (root, schema, options = {}) {
  67. Common.assertOptions(options, ['legacy']);
  68. // Compiled by any supported version
  69. const any = schema && schema[Common.symbols.any];
  70. if (any) {
  71. Assert(options.legacy || any.version === Common.version, 'Cannot mix different versions of joi schemas:', any.version, Common.version);
  72. return schema;
  73. }
  74. // Uncompiled root
  75. if (typeof schema !== 'object' ||
  76. !options.legacy) {
  77. return exports.schema(root, schema, { appendPath: true }); // Will error if schema contains other versions
  78. }
  79. // Scan schema for compiled parts
  80. const compiler = internals.walk(schema);
  81. if (!compiler) {
  82. return exports.schema(root, schema, { appendPath: true });
  83. }
  84. return compiler.compile(compiler.root, schema);
  85. };
  86. internals.walk = function (schema) {
  87. if (typeof schema !== 'object') {
  88. return null;
  89. }
  90. if (Array.isArray(schema)) {
  91. for (const item of schema) {
  92. const compiler = internals.walk(item);
  93. if (compiler) {
  94. return compiler;
  95. }
  96. }
  97. return null;
  98. }
  99. const any = schema[Common.symbols.any];
  100. if (any) {
  101. return { root: schema[any.root], compile: any.compile };
  102. }
  103. Assert(Object.getPrototypeOf(schema) === Object.getPrototypeOf({}), 'Schema can only contain plain objects');
  104. for (const key in schema) {
  105. const compiler = internals.walk(schema[key]);
  106. if (compiler) {
  107. return compiler;
  108. }
  109. }
  110. return null;
  111. };
  112. internals.simple = function (value) {
  113. return value === null || ['boolean', 'string', 'number'].includes(typeof value);
  114. };
  115. exports.when = function (schema, condition, options) {
  116. if (options === undefined) {
  117. Assert(condition && typeof condition === 'object', 'Missing options');
  118. options = condition;
  119. condition = Ref.create('.');
  120. }
  121. if (Array.isArray(options)) {
  122. options = { switch: options };
  123. }
  124. Common.assertOptions(options, ['is', 'not', 'then', 'otherwise', 'switch', 'break']);
  125. // Schema condition
  126. if (Common.isSchema(condition)) {
  127. Assert(options.is === undefined, '"is" can not be used with a schema condition');
  128. Assert(options.not === undefined, '"not" can not be used with a schema condition');
  129. Assert(options.switch === undefined, '"switch" can not be used with a schema condition');
  130. return internals.condition(schema, { is: condition, then: options.then, otherwise: options.otherwise, break: options.break });
  131. }
  132. // Single condition
  133. Assert(Ref.isRef(condition) || typeof condition === 'string', 'Invalid condition:', condition);
  134. Assert(options.not === undefined || options.is === undefined, 'Cannot combine "is" with "not"');
  135. if (options.switch === undefined) {
  136. let rule = options;
  137. if (options.not !== undefined) {
  138. rule = { is: options.not, then: options.otherwise, otherwise: options.then, break: options.break };
  139. }
  140. let is = rule.is !== undefined ? schema.$_compile(rule.is) : schema.$_root.invalid(null, false, 0, '').required();
  141. Assert(rule.then !== undefined || rule.otherwise !== undefined, 'options must have at least one of "then", "otherwise", or "switch"');
  142. Assert(rule.break === undefined || rule.then === undefined || rule.otherwise === undefined, 'Cannot specify then, otherwise, and break all together');
  143. if (options.is !== undefined &&
  144. !Ref.isRef(options.is) &&
  145. !Common.isSchema(options.is)) {
  146. is = is.required(); // Only apply required if this wasn't already a schema or a ref
  147. }
  148. return internals.condition(schema, { ref: exports.ref(condition), is, then: rule.then, otherwise: rule.otherwise, break: rule.break });
  149. }
  150. // Switch statement
  151. Assert(Array.isArray(options.switch), '"switch" must be an array');
  152. Assert(options.is === undefined, 'Cannot combine "switch" with "is"');
  153. Assert(options.not === undefined, 'Cannot combine "switch" with "not"');
  154. Assert(options.then === undefined, 'Cannot combine "switch" with "then"');
  155. const rule = {
  156. ref: exports.ref(condition),
  157. switch: [],
  158. break: options.break
  159. };
  160. for (let i = 0; i < options.switch.length; ++i) {
  161. const test = options.switch[i];
  162. const last = i === options.switch.length - 1;
  163. Common.assertOptions(test, last ? ['is', 'then', 'otherwise'] : ['is', 'then']);
  164. Assert(test.is !== undefined, 'Switch statement missing "is"');
  165. Assert(test.then !== undefined, 'Switch statement missing "then"');
  166. const item = {
  167. is: schema.$_compile(test.is),
  168. then: schema.$_compile(test.then)
  169. };
  170. if (!Ref.isRef(test.is) &&
  171. !Common.isSchema(test.is)) {
  172. item.is = item.is.required(); // Only apply required if this wasn't already a schema or a ref
  173. }
  174. if (last) {
  175. Assert(options.otherwise === undefined || test.otherwise === undefined, 'Cannot specify "otherwise" inside and outside a "switch"');
  176. const otherwise = options.otherwise !== undefined ? options.otherwise : test.otherwise;
  177. if (otherwise !== undefined) {
  178. Assert(rule.break === undefined, 'Cannot specify both otherwise and break');
  179. item.otherwise = schema.$_compile(otherwise);
  180. }
  181. }
  182. rule.switch.push(item);
  183. }
  184. return rule;
  185. };
  186. internals.condition = function (schema, condition) {
  187. for (const key of ['then', 'otherwise']) {
  188. if (condition[key] === undefined) {
  189. delete condition[key];
  190. }
  191. else {
  192. condition[key] = schema.$_compile(condition[key]);
  193. }
  194. }
  195. return condition;
  196. };