ValidationError.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. const {
  7. stringHints,
  8. numberHints
  9. } = require("./util/hints");
  10. /** @typedef {import("json-schema").JSONSchema6} JSONSchema6 */
  11. /** @typedef {import("json-schema").JSONSchema7} JSONSchema7 */
  12. /** @typedef {import("./validate").Schema} Schema */
  13. /** @typedef {import("./validate").ValidationErrorConfiguration} ValidationErrorConfiguration */
  14. /** @typedef {import("./validate").PostFormatter} PostFormatter */
  15. /** @typedef {import("./validate").SchemaUtilErrorObject} SchemaUtilErrorObject */
  16. /** @enum {number} */
  17. const SPECIFICITY = {
  18. type: 1,
  19. not: 1,
  20. oneOf: 1,
  21. anyOf: 1,
  22. if: 1,
  23. enum: 1,
  24. const: 1,
  25. instanceof: 1,
  26. required: 2,
  27. pattern: 2,
  28. patternRequired: 2,
  29. format: 2,
  30. formatMinimum: 2,
  31. formatMaximum: 2,
  32. minimum: 2,
  33. exclusiveMinimum: 2,
  34. maximum: 2,
  35. exclusiveMaximum: 2,
  36. multipleOf: 2,
  37. uniqueItems: 2,
  38. contains: 2,
  39. minLength: 2,
  40. maxLength: 2,
  41. minItems: 2,
  42. maxItems: 2,
  43. minProperties: 2,
  44. maxProperties: 2,
  45. dependencies: 2,
  46. propertyNames: 2,
  47. additionalItems: 2,
  48. additionalProperties: 2,
  49. absolutePath: 2
  50. };
  51. /**
  52. * @param {string} value
  53. * @returns {value is number}
  54. */
  55. function isNumeric(value) {
  56. return /^-?\d+$/.test(value);
  57. }
  58. /**
  59. *
  60. * @param {Array<SchemaUtilErrorObject>} array
  61. * @param {(item: SchemaUtilErrorObject) => number} fn
  62. * @returns {Array<SchemaUtilErrorObject>}
  63. */
  64. function filterMax(array, fn) {
  65. const evaluatedMax = array.reduce((max, item) => Math.max(max, fn(item)), 0);
  66. return array.filter(item => fn(item) === evaluatedMax);
  67. }
  68. /**
  69. *
  70. * @param {Array<SchemaUtilErrorObject>} children
  71. * @returns {Array<SchemaUtilErrorObject>}
  72. */
  73. function filterChildren(children) {
  74. let newChildren = children;
  75. newChildren = filterMax(newChildren,
  76. /**
  77. *
  78. * @param {SchemaUtilErrorObject} error
  79. * @returns {number}
  80. */
  81. error => error.instancePath ? error.instancePath.length : 0);
  82. newChildren = filterMax(newChildren,
  83. /**
  84. * @param {SchemaUtilErrorObject} error
  85. * @returns {number}
  86. */
  87. error => SPECIFICITY[
  88. /** @type {keyof typeof SPECIFICITY} */
  89. error.keyword] || 2);
  90. return newChildren;
  91. }
  92. /**
  93. * Find all children errors
  94. * @param {Array<SchemaUtilErrorObject>} children
  95. * @param {Array<string>} schemaPaths
  96. * @return {number} returns index of first child
  97. */
  98. function findAllChildren(children, schemaPaths) {
  99. let i = children.length - 1;
  100. const predicate =
  101. /**
  102. * @param {string} schemaPath
  103. * @returns {boolean}
  104. */
  105. schemaPath => children[i].schemaPath.indexOf(schemaPath) !== 0;
  106. while (i > -1 && !schemaPaths.every(predicate)) {
  107. if (children[i].keyword === "anyOf" || children[i].keyword === "oneOf") {
  108. const refs = extractRefs(children[i]);
  109. const childrenStart = findAllChildren(children.slice(0, i), refs.concat(children[i].schemaPath));
  110. i = childrenStart - 1;
  111. } else {
  112. i -= 1;
  113. }
  114. }
  115. return i + 1;
  116. }
  117. /**
  118. * Extracts all refs from schema
  119. * @param {SchemaUtilErrorObject} error
  120. * @return {Array<string>}
  121. */
  122. function extractRefs(error) {
  123. const {
  124. schema
  125. } = error;
  126. if (!Array.isArray(schema)) {
  127. return [];
  128. }
  129. return schema.map(({
  130. $ref
  131. }) => $ref).filter(s => s);
  132. }
  133. /**
  134. * Groups children by their first level parent (assuming that error is root)
  135. * @param {Array<SchemaUtilErrorObject>} children
  136. * @return {Array<SchemaUtilErrorObject>}
  137. */
  138. function groupChildrenByFirstChild(children) {
  139. const result = [];
  140. let i = children.length - 1;
  141. while (i > 0) {
  142. const child = children[i];
  143. if (child.keyword === "anyOf" || child.keyword === "oneOf") {
  144. const refs = extractRefs(child);
  145. const childrenStart = findAllChildren(children.slice(0, i), refs.concat(child.schemaPath));
  146. if (childrenStart !== i) {
  147. result.push(Object.assign({}, child, {
  148. children: children.slice(childrenStart, i)
  149. }));
  150. i = childrenStart;
  151. } else {
  152. result.push(child);
  153. }
  154. } else {
  155. result.push(child);
  156. }
  157. i -= 1;
  158. }
  159. if (i === 0) {
  160. result.push(children[i]);
  161. }
  162. return result.reverse();
  163. }
  164. /**
  165. * @param {string} str
  166. * @param {string} prefix
  167. * @returns {string}
  168. */
  169. function indent(str, prefix) {
  170. return str.replace(/\n(?!$)/g, `\n${prefix}`);
  171. }
  172. /**
  173. * @param {Schema} schema
  174. * @returns {schema is (Schema & {not: Schema})}
  175. */
  176. function hasNotInSchema(schema) {
  177. return !!schema.not;
  178. }
  179. /**
  180. * @param {Schema} schema
  181. * @return {Schema}
  182. */
  183. function findFirstTypedSchema(schema) {
  184. if (hasNotInSchema(schema)) {
  185. return findFirstTypedSchema(schema.not);
  186. }
  187. return schema;
  188. }
  189. /**
  190. * @param {Schema} schema
  191. * @return {boolean}
  192. */
  193. function canApplyNot(schema) {
  194. const typedSchema = findFirstTypedSchema(schema);
  195. return likeNumber(typedSchema) || likeInteger(typedSchema) || likeString(typedSchema) || likeNull(typedSchema) || likeBoolean(typedSchema);
  196. }
  197. /**
  198. * @param {any} maybeObj
  199. * @returns {boolean}
  200. */
  201. function isObject(maybeObj) {
  202. return typeof maybeObj === "object" && maybeObj !== null;
  203. }
  204. /**
  205. * @param {Schema} schema
  206. * @returns {boolean}
  207. */
  208. function likeNumber(schema) {
  209. return schema.type === "number" || typeof schema.minimum !== "undefined" || typeof schema.exclusiveMinimum !== "undefined" || typeof schema.maximum !== "undefined" || typeof schema.exclusiveMaximum !== "undefined" || typeof schema.multipleOf !== "undefined";
  210. }
  211. /**
  212. * @param {Schema} schema
  213. * @returns {boolean}
  214. */
  215. function likeInteger(schema) {
  216. return schema.type === "integer" || typeof schema.minimum !== "undefined" || typeof schema.exclusiveMinimum !== "undefined" || typeof schema.maximum !== "undefined" || typeof schema.exclusiveMaximum !== "undefined" || typeof schema.multipleOf !== "undefined";
  217. }
  218. /**
  219. * @param {Schema} schema
  220. * @returns {boolean}
  221. */
  222. function likeString(schema) {
  223. return schema.type === "string" || typeof schema.minLength !== "undefined" || typeof schema.maxLength !== "undefined" || typeof schema.pattern !== "undefined" || typeof schema.format !== "undefined" || typeof schema.formatMinimum !== "undefined" || typeof schema.formatMaximum !== "undefined";
  224. }
  225. /**
  226. * @param {Schema} schema
  227. * @returns {boolean}
  228. */
  229. function likeBoolean(schema) {
  230. return schema.type === "boolean";
  231. }
  232. /**
  233. * @param {Schema} schema
  234. * @returns {boolean}
  235. */
  236. function likeArray(schema) {
  237. return schema.type === "array" || typeof schema.minItems === "number" || typeof schema.maxItems === "number" || typeof schema.uniqueItems !== "undefined" || typeof schema.items !== "undefined" || typeof schema.additionalItems !== "undefined" || typeof schema.contains !== "undefined";
  238. }
  239. /**
  240. * @param {Schema & {patternRequired?: Array<string>}} schema
  241. * @returns {boolean}
  242. */
  243. function likeObject(schema) {
  244. return schema.type === "object" || typeof schema.minProperties !== "undefined" || typeof schema.maxProperties !== "undefined" || typeof schema.required !== "undefined" || typeof schema.properties !== "undefined" || typeof schema.patternProperties !== "undefined" || typeof schema.additionalProperties !== "undefined" || typeof schema.dependencies !== "undefined" || typeof schema.propertyNames !== "undefined" || typeof schema.patternRequired !== "undefined";
  245. }
  246. /**
  247. * @param {Schema} schema
  248. * @returns {boolean}
  249. */
  250. function likeNull(schema) {
  251. return schema.type === "null";
  252. }
  253. /**
  254. * @param {string} type
  255. * @returns {string}
  256. */
  257. function getArticle(type) {
  258. if (/^[aeiou]/i.test(type)) {
  259. return "an";
  260. }
  261. return "a";
  262. }
  263. /**
  264. * @param {Schema=} schema
  265. * @returns {string}
  266. */
  267. function getSchemaNonTypes(schema) {
  268. if (!schema) {
  269. return "";
  270. }
  271. if (!schema.type) {
  272. if (likeNumber(schema) || likeInteger(schema)) {
  273. return " | should be any non-number";
  274. }
  275. if (likeString(schema)) {
  276. return " | should be any non-string";
  277. }
  278. if (likeArray(schema)) {
  279. return " | should be any non-array";
  280. }
  281. if (likeObject(schema)) {
  282. return " | should be any non-object";
  283. }
  284. }
  285. return "";
  286. }
  287. /**
  288. * @param {Array<string>} hints
  289. * @returns {string}
  290. */
  291. function formatHints(hints) {
  292. return hints.length > 0 ? `(${hints.join(", ")})` : "";
  293. }
  294. /**
  295. * @param {Schema} schema
  296. * @param {boolean} logic
  297. * @returns {string[]}
  298. */
  299. function getHints(schema, logic) {
  300. if (likeNumber(schema) || likeInteger(schema)) {
  301. return numberHints(schema, logic);
  302. } else if (likeString(schema)) {
  303. return stringHints(schema, logic);
  304. }
  305. return [];
  306. }
  307. class ValidationError extends Error {
  308. /**
  309. * @param {Array<SchemaUtilErrorObject>} errors
  310. * @param {Schema} schema
  311. * @param {ValidationErrorConfiguration} configuration
  312. */
  313. constructor(errors, schema, configuration = {}) {
  314. super();
  315. /** @type {string} */
  316. this.name = "ValidationError";
  317. /** @type {Array<SchemaUtilErrorObject>} */
  318. this.errors = errors;
  319. /** @type {Schema} */
  320. this.schema = schema;
  321. let headerNameFromSchema;
  322. let baseDataPathFromSchema;
  323. if (schema.title && (!configuration.name || !configuration.baseDataPath)) {
  324. const splittedTitleFromSchema = schema.title.match(/^(.+) (.+)$/);
  325. if (splittedTitleFromSchema) {
  326. if (!configuration.name) {
  327. [, headerNameFromSchema] = splittedTitleFromSchema;
  328. }
  329. if (!configuration.baseDataPath) {
  330. [,, baseDataPathFromSchema] = splittedTitleFromSchema;
  331. }
  332. }
  333. }
  334. /** @type {string} */
  335. this.headerName = configuration.name || headerNameFromSchema || "Object";
  336. /** @type {string} */
  337. this.baseDataPath = configuration.baseDataPath || baseDataPathFromSchema || "configuration";
  338. /** @type {PostFormatter | null} */
  339. this.postFormatter = configuration.postFormatter || null;
  340. const header = `Invalid ${this.baseDataPath} object. ${this.headerName} has been initialized using ${getArticle(this.baseDataPath)} ${this.baseDataPath} object that does not match the API schema.\n`;
  341. /** @type {string} */
  342. this.message = `${header}${this.formatValidationErrors(errors)}`;
  343. Error.captureStackTrace(this, this.constructor);
  344. }
  345. /**
  346. * @param {string} path
  347. * @returns {Schema}
  348. */
  349. getSchemaPart(path) {
  350. const newPath = path.split("/");
  351. let schemaPart = this.schema;
  352. for (let i = 1; i < newPath.length; i++) {
  353. const inner = schemaPart[
  354. /** @type {keyof Schema} */
  355. newPath[i]];
  356. if (!inner) {
  357. break;
  358. }
  359. schemaPart = inner;
  360. }
  361. return schemaPart;
  362. }
  363. /**
  364. * @param {Schema} schema
  365. * @param {boolean} logic
  366. * @param {Array<Object>} prevSchemas
  367. * @returns {string}
  368. */
  369. formatSchema(schema, logic = true, prevSchemas = []) {
  370. let newLogic = logic;
  371. const formatInnerSchema =
  372. /**
  373. *
  374. * @param {Object} innerSchema
  375. * @param {boolean=} addSelf
  376. * @returns {string}
  377. */
  378. (innerSchema, addSelf) => {
  379. if (!addSelf) {
  380. return this.formatSchema(innerSchema, newLogic, prevSchemas);
  381. }
  382. if (prevSchemas.includes(innerSchema)) {
  383. return "(recursive)";
  384. }
  385. return this.formatSchema(innerSchema, newLogic, prevSchemas.concat(schema));
  386. };
  387. if (hasNotInSchema(schema) && !likeObject(schema)) {
  388. if (canApplyNot(schema.not)) {
  389. newLogic = !logic;
  390. return formatInnerSchema(schema.not);
  391. }
  392. const needApplyLogicHere = !schema.not.not;
  393. const prefix = logic ? "" : "non ";
  394. newLogic = !logic;
  395. return needApplyLogicHere ? prefix + formatInnerSchema(schema.not) : formatInnerSchema(schema.not);
  396. }
  397. if (
  398. /** @type {Schema & {instanceof: string | Array<string>}} */
  399. schema.instanceof) {
  400. const {
  401. instanceof: value
  402. } =
  403. /** @type {Schema & {instanceof: string | Array<string>}} */
  404. schema;
  405. const values = !Array.isArray(value) ? [value] : value;
  406. return values.map(
  407. /**
  408. * @param {string} item
  409. * @returns {string}
  410. */
  411. item => item === "Function" ? "function" : item).join(" | ");
  412. }
  413. if (schema.enum) {
  414. return (
  415. /** @type {Array<any>} */
  416. schema.enum.map(item => JSON.stringify(item)).join(" | ")
  417. );
  418. }
  419. if (typeof schema.const !== "undefined") {
  420. return JSON.stringify(schema.const);
  421. }
  422. if (schema.oneOf) {
  423. return (
  424. /** @type {Array<Schema>} */
  425. schema.oneOf.map(item => formatInnerSchema(item, true)).join(" | ")
  426. );
  427. }
  428. if (schema.anyOf) {
  429. return (
  430. /** @type {Array<Schema>} */
  431. schema.anyOf.map(item => formatInnerSchema(item, true)).join(" | ")
  432. );
  433. }
  434. if (schema.allOf) {
  435. return (
  436. /** @type {Array<Schema>} */
  437. schema.allOf.map(item => formatInnerSchema(item, true)).join(" & ")
  438. );
  439. }
  440. if (
  441. /** @type {JSONSchema7} */
  442. schema.if) {
  443. const {
  444. if: ifValue,
  445. then: thenValue,
  446. else: elseValue
  447. } =
  448. /** @type {JSONSchema7} */
  449. schema;
  450. return `${ifValue ? `if ${formatInnerSchema(ifValue)}` : ""}${thenValue ? ` then ${formatInnerSchema(thenValue)}` : ""}${elseValue ? ` else ${formatInnerSchema(elseValue)}` : ""}`;
  451. }
  452. if (schema.$ref) {
  453. return formatInnerSchema(this.getSchemaPart(schema.$ref), true);
  454. }
  455. if (likeNumber(schema) || likeInteger(schema)) {
  456. const [type, ...hints] = getHints(schema, logic);
  457. const str = `${type}${hints.length > 0 ? ` ${formatHints(hints)}` : ""}`;
  458. return logic ? str : hints.length > 0 ? `non-${type} | ${str}` : `non-${type}`;
  459. }
  460. if (likeString(schema)) {
  461. const [type, ...hints] = getHints(schema, logic);
  462. const str = `${type}${hints.length > 0 ? ` ${formatHints(hints)}` : ""}`;
  463. return logic ? str : str === "string" ? "non-string" : `non-string | ${str}`;
  464. }
  465. if (likeBoolean(schema)) {
  466. return `${logic ? "" : "non-"}boolean`;
  467. }
  468. if (likeArray(schema)) {
  469. // not logic already applied in formatValidationError
  470. newLogic = true;
  471. const hints = [];
  472. if (typeof schema.minItems === "number") {
  473. hints.push(`should not have fewer than ${schema.minItems} item${schema.minItems > 1 ? "s" : ""}`);
  474. }
  475. if (typeof schema.maxItems === "number") {
  476. hints.push(`should not have more than ${schema.maxItems} item${schema.maxItems > 1 ? "s" : ""}`);
  477. }
  478. if (schema.uniqueItems) {
  479. hints.push("should not have duplicate items");
  480. }
  481. const hasAdditionalItems = typeof schema.additionalItems === "undefined" || Boolean(schema.additionalItems);
  482. let items = "";
  483. if (schema.items) {
  484. if (Array.isArray(schema.items) && schema.items.length > 0) {
  485. items = `${
  486. /** @type {Array<Schema>} */
  487. schema.items.map(item => formatInnerSchema(item)).join(", ")}`;
  488. if (hasAdditionalItems) {
  489. if (schema.additionalItems && isObject(schema.additionalItems) && Object.keys(schema.additionalItems).length > 0) {
  490. hints.push(`additional items should be ${formatInnerSchema(schema.additionalItems)}`);
  491. }
  492. }
  493. } else if (schema.items && Object.keys(schema.items).length > 0) {
  494. // "additionalItems" is ignored
  495. items = `${formatInnerSchema(schema.items)}`;
  496. } else {
  497. // Fallback for empty `items` value
  498. items = "any";
  499. }
  500. } else {
  501. // "additionalItems" is ignored
  502. items = "any";
  503. }
  504. if (schema.contains && Object.keys(schema.contains).length > 0) {
  505. hints.push(`should contains at least one ${this.formatSchema(schema.contains)} item`);
  506. }
  507. return `[${items}${hasAdditionalItems ? ", ..." : ""}]${hints.length > 0 ? ` (${hints.join(", ")})` : ""}`;
  508. }
  509. if (likeObject(schema)) {
  510. // not logic already applied in formatValidationError
  511. newLogic = true;
  512. const hints = [];
  513. if (typeof schema.minProperties === "number") {
  514. hints.push(`should not have fewer than ${schema.minProperties} ${schema.minProperties > 1 ? "properties" : "property"}`);
  515. }
  516. if (typeof schema.maxProperties === "number") {
  517. hints.push(`should not have more than ${schema.maxProperties} ${schema.minProperties && schema.minProperties > 1 ? "properties" : "property"}`);
  518. }
  519. if (schema.patternProperties && Object.keys(schema.patternProperties).length > 0) {
  520. const patternProperties = Object.keys(schema.patternProperties);
  521. hints.push(`additional property names should match pattern${patternProperties.length > 1 ? "s" : ""} ${patternProperties.map(pattern => JSON.stringify(pattern)).join(" | ")}`);
  522. }
  523. const properties = schema.properties ? Object.keys(schema.properties) : [];
  524. /** @type {Array<string>} */
  525. // @ts-ignore
  526. const required = schema.required ? schema.required : [];
  527. const allProperties = [...new Set(
  528. /** @type {Array<string>} */
  529. [].concat(required).concat(properties))];
  530. const objectStructure = allProperties.map(property => {
  531. const isRequired = required.includes(property); // Some properties need quotes, maybe we should add check
  532. // Maybe we should output type of property (`foo: string`), but it is looks very unreadable
  533. return `${property}${isRequired ? "" : "?"}`;
  534. }).concat(typeof schema.additionalProperties === "undefined" || Boolean(schema.additionalProperties) ? schema.additionalProperties && isObject(schema.additionalProperties) ? [`<key>: ${formatInnerSchema(schema.additionalProperties)}`] : ["…"] : []).join(", ");
  535. const {
  536. dependencies,
  537. propertyNames,
  538. patternRequired
  539. } =
  540. /** @type {Schema & {patternRequired?: Array<string>;}} */
  541. schema;
  542. if (dependencies) {
  543. Object.keys(dependencies).forEach(dependencyName => {
  544. const dependency = dependencies[dependencyName];
  545. if (Array.isArray(dependency)) {
  546. hints.push(`should have ${dependency.length > 1 ? "properties" : "property"} ${dependency.map(dep => `'${dep}'`).join(", ")} when property '${dependencyName}' is present`);
  547. } else {
  548. hints.push(`should be valid according to the schema ${formatInnerSchema(dependency)} when property '${dependencyName}' is present`);
  549. }
  550. });
  551. }
  552. if (propertyNames && Object.keys(propertyNames).length > 0) {
  553. hints.push(`each property name should match format ${JSON.stringify(schema.propertyNames.format)}`);
  554. }
  555. if (patternRequired && patternRequired.length > 0) {
  556. hints.push(`should have property matching pattern ${patternRequired.map(
  557. /**
  558. * @param {string} item
  559. * @returns {string}
  560. */
  561. item => JSON.stringify(item))}`);
  562. }
  563. return `object {${objectStructure ? ` ${objectStructure} ` : ""}}${hints.length > 0 ? ` (${hints.join(", ")})` : ""}`;
  564. }
  565. if (likeNull(schema)) {
  566. return `${logic ? "" : "non-"}null`;
  567. }
  568. if (Array.isArray(schema.type)) {
  569. // not logic already applied in formatValidationError
  570. return `${schema.type.join(" | ")}`;
  571. } // Fallback for unknown keywords
  572. // not logic already applied in formatValidationError
  573. /* istanbul ignore next */
  574. return JSON.stringify(schema, null, 2);
  575. }
  576. /**
  577. * @param {Schema=} schemaPart
  578. * @param {(boolean | Array<string>)=} additionalPath
  579. * @param {boolean=} needDot
  580. * @param {boolean=} logic
  581. * @returns {string}
  582. */
  583. getSchemaPartText(schemaPart, additionalPath, needDot = false, logic = true) {
  584. if (!schemaPart) {
  585. return "";
  586. }
  587. if (Array.isArray(additionalPath)) {
  588. for (let i = 0; i < additionalPath.length; i++) {
  589. /** @type {Schema | undefined} */
  590. const inner = schemaPart[
  591. /** @type {keyof Schema} */
  592. additionalPath[i]];
  593. if (inner) {
  594. // eslint-disable-next-line no-param-reassign
  595. schemaPart = inner;
  596. } else {
  597. break;
  598. }
  599. }
  600. }
  601. while (schemaPart.$ref) {
  602. // eslint-disable-next-line no-param-reassign
  603. schemaPart = this.getSchemaPart(schemaPart.$ref);
  604. }
  605. let schemaText = `${this.formatSchema(schemaPart, logic)}${needDot ? "." : ""}`;
  606. if (schemaPart.description) {
  607. schemaText += `\n-> ${schemaPart.description}`;
  608. }
  609. if (schemaPart.link) {
  610. schemaText += `\n-> Read more at ${schemaPart.link}`;
  611. }
  612. return schemaText;
  613. }
  614. /**
  615. * @param {Schema=} schemaPart
  616. * @returns {string}
  617. */
  618. getSchemaPartDescription(schemaPart) {
  619. if (!schemaPart) {
  620. return "";
  621. }
  622. while (schemaPart.$ref) {
  623. // eslint-disable-next-line no-param-reassign
  624. schemaPart = this.getSchemaPart(schemaPart.$ref);
  625. }
  626. let schemaText = "";
  627. if (schemaPart.description) {
  628. schemaText += `\n-> ${schemaPart.description}`;
  629. }
  630. if (schemaPart.link) {
  631. schemaText += `\n-> Read more at ${schemaPart.link}`;
  632. }
  633. return schemaText;
  634. }
  635. /**
  636. * @param {SchemaUtilErrorObject} error
  637. * @returns {string}
  638. */
  639. formatValidationError(error) {
  640. const {
  641. keyword,
  642. instancePath: errorInstancePath
  643. } = error;
  644. const splittedInstancePath = errorInstancePath.split("/");
  645. /**
  646. * @type {Array<string>}
  647. */
  648. const defaultValue = [];
  649. const prettyInstancePath = splittedInstancePath.reduce((acc, val) => {
  650. if (val.length > 0) {
  651. if (isNumeric(val)) {
  652. acc.push(`[${val}]`);
  653. } else if (/^\[/.test(val)) {
  654. acc.push(val);
  655. } else {
  656. acc.push(`.${val}`);
  657. }
  658. }
  659. return acc;
  660. }, defaultValue).join("");
  661. const instancePath = `${this.baseDataPath}${prettyInstancePath}`; // const { keyword, instancePath: errorInstancePath } = error;
  662. // const instancePath = `${this.baseDataPath}${errorInstancePath.replace(/\//g, '.')}`;
  663. switch (keyword) {
  664. case "type":
  665. {
  666. const {
  667. parentSchema,
  668. params
  669. } = error; // eslint-disable-next-line default-case
  670. switch (params.type) {
  671. case "number":
  672. return `${instancePath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
  673. case "integer":
  674. return `${instancePath} should be an ${this.getSchemaPartText(parentSchema, false, true)}`;
  675. case "string":
  676. return `${instancePath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
  677. case "boolean":
  678. return `${instancePath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
  679. case "array":
  680. return `${instancePath} should be an array:\n${this.getSchemaPartText(parentSchema)}`;
  681. case "object":
  682. return `${instancePath} should be an object:\n${this.getSchemaPartText(parentSchema)}`;
  683. case "null":
  684. return `${instancePath} should be a ${this.getSchemaPartText(parentSchema, false, true)}`;
  685. default:
  686. return `${instancePath} should be:\n${this.getSchemaPartText(parentSchema)}`;
  687. }
  688. }
  689. case "instanceof":
  690. {
  691. const {
  692. parentSchema
  693. } = error;
  694. return `${instancePath} should be an instance of ${this.getSchemaPartText(parentSchema, false, true)}`;
  695. }
  696. case "pattern":
  697. {
  698. const {
  699. params,
  700. parentSchema
  701. } = error;
  702. const {
  703. pattern
  704. } = params;
  705. return `${instancePath} should match pattern ${JSON.stringify(pattern)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  706. }
  707. case "format":
  708. {
  709. const {
  710. params,
  711. parentSchema
  712. } = error;
  713. const {
  714. format
  715. } = params;
  716. return `${instancePath} should match format ${JSON.stringify(format)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  717. }
  718. case "formatMinimum":
  719. case "formatExclusiveMinimum":
  720. case "formatMaximum":
  721. case "formatExclusiveMaximum":
  722. {
  723. const {
  724. params,
  725. parentSchema
  726. } = error;
  727. const {
  728. comparison,
  729. limit
  730. } = params;
  731. return `${instancePath} should be ${comparison} ${JSON.stringify(limit)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  732. }
  733. case "minimum":
  734. case "maximum":
  735. case "exclusiveMinimum":
  736. case "exclusiveMaximum":
  737. {
  738. const {
  739. parentSchema,
  740. params
  741. } = error;
  742. const {
  743. comparison,
  744. limit
  745. } = params;
  746. const [, ...hints] = getHints(
  747. /** @type {Schema} */
  748. parentSchema, true);
  749. if (hints.length === 0) {
  750. hints.push(`should be ${comparison} ${limit}`);
  751. }
  752. return `${instancePath} ${hints.join(" ")}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  753. }
  754. case "multipleOf":
  755. {
  756. const {
  757. params,
  758. parentSchema
  759. } = error;
  760. const {
  761. multipleOf
  762. } = params;
  763. return `${instancePath} should be multiple of ${multipleOf}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  764. }
  765. case "patternRequired":
  766. {
  767. const {
  768. params,
  769. parentSchema
  770. } = error;
  771. const {
  772. missingPattern
  773. } = params;
  774. return `${instancePath} should have property matching pattern ${JSON.stringify(missingPattern)}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  775. }
  776. case "minLength":
  777. {
  778. const {
  779. params,
  780. parentSchema
  781. } = error;
  782. const {
  783. limit
  784. } = params;
  785. if (limit === 1) {
  786. return `${instancePath} should be a non-empty string${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  787. }
  788. const length = limit - 1;
  789. return `${instancePath} should be longer than ${length} character${length > 1 ? "s" : ""}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  790. }
  791. case "minItems":
  792. {
  793. const {
  794. params,
  795. parentSchema
  796. } = error;
  797. const {
  798. limit
  799. } = params;
  800. if (limit === 1) {
  801. return `${instancePath} should be a non-empty array${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  802. }
  803. return `${instancePath} should not have fewer than ${limit} items${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  804. }
  805. case "minProperties":
  806. {
  807. const {
  808. params,
  809. parentSchema
  810. } = error;
  811. const {
  812. limit
  813. } = params;
  814. if (limit === 1) {
  815. return `${instancePath} should be a non-empty object${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  816. }
  817. return `${instancePath} should not have fewer than ${limit} properties${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  818. }
  819. case "maxLength":
  820. {
  821. const {
  822. params,
  823. parentSchema
  824. } = error;
  825. const {
  826. limit
  827. } = params;
  828. const max = limit + 1;
  829. return `${instancePath} should be shorter than ${max} character${max > 1 ? "s" : ""}${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  830. }
  831. case "maxItems":
  832. {
  833. const {
  834. params,
  835. parentSchema
  836. } = error;
  837. const {
  838. limit
  839. } = params;
  840. return `${instancePath} should not have more than ${limit} items${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  841. }
  842. case "maxProperties":
  843. {
  844. const {
  845. params,
  846. parentSchema
  847. } = error;
  848. const {
  849. limit
  850. } = params;
  851. return `${instancePath} should not have more than ${limit} properties${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  852. }
  853. case "uniqueItems":
  854. {
  855. const {
  856. params,
  857. parentSchema
  858. } = error;
  859. const {
  860. i
  861. } = params;
  862. return `${instancePath} should not contain the item '${
  863. /** @type {{ data: Array<any> }} **/
  864. error.data[i]}' twice${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  865. }
  866. case "additionalItems":
  867. {
  868. const {
  869. params,
  870. parentSchema
  871. } = error;
  872. const {
  873. limit
  874. } = params;
  875. return `${instancePath} should not have more than ${limit} items${getSchemaNonTypes(parentSchema)}. These items are valid:\n${this.getSchemaPartText(parentSchema)}`;
  876. }
  877. case "contains":
  878. {
  879. const {
  880. parentSchema
  881. } = error;
  882. return `${instancePath} should contains at least one ${this.getSchemaPartText(parentSchema, ["contains"])} item${getSchemaNonTypes(parentSchema)}.`;
  883. }
  884. case "required":
  885. {
  886. const {
  887. parentSchema,
  888. params
  889. } = error;
  890. const missingProperty = params.missingProperty.replace(/^\./, "");
  891. const hasProperty = parentSchema && Boolean(
  892. /** @type {Schema} */
  893. parentSchema.properties &&
  894. /** @type {Schema} */
  895. parentSchema.properties[missingProperty]);
  896. return `${instancePath} misses the property '${missingProperty}'${getSchemaNonTypes(parentSchema)}.${hasProperty ? ` Should be:\n${this.getSchemaPartText(parentSchema, ["properties", missingProperty])}` : this.getSchemaPartDescription(parentSchema)}`;
  897. }
  898. case "additionalProperties":
  899. {
  900. const {
  901. params,
  902. parentSchema
  903. } = error;
  904. const {
  905. additionalProperty
  906. } = params;
  907. return `${instancePath} has an unknown property '${additionalProperty}'${getSchemaNonTypes(parentSchema)}. These properties are valid:\n${this.getSchemaPartText(parentSchema)}`;
  908. }
  909. case "dependencies":
  910. {
  911. const {
  912. params,
  913. parentSchema
  914. } = error;
  915. const {
  916. property,
  917. deps
  918. } = params;
  919. const dependencies = deps.split(",").map(
  920. /**
  921. * @param {string} dep
  922. * @returns {string}
  923. */
  924. dep => `'${dep.trim()}'`).join(", ");
  925. return `${instancePath} should have properties ${dependencies} when property '${property}' is present${getSchemaNonTypes(parentSchema)}.${this.getSchemaPartDescription(parentSchema)}`;
  926. }
  927. case "propertyNames":
  928. {
  929. const {
  930. params,
  931. parentSchema,
  932. schema
  933. } = error;
  934. const {
  935. propertyName
  936. } = params;
  937. return `${instancePath} property name '${propertyName}' is invalid${getSchemaNonTypes(parentSchema)}. Property names should be match format ${JSON.stringify(schema.format)}.${this.getSchemaPartDescription(parentSchema)}`;
  938. }
  939. case "enum":
  940. {
  941. const {
  942. parentSchema
  943. } = error;
  944. if (parentSchema &&
  945. /** @type {Schema} */
  946. parentSchema.enum &&
  947. /** @type {Schema} */
  948. parentSchema.enum.length === 1) {
  949. return `${instancePath} should be ${this.getSchemaPartText(parentSchema, false, true)}`;
  950. }
  951. return `${instancePath} should be one of these:\n${this.getSchemaPartText(parentSchema)}`;
  952. }
  953. case "const":
  954. {
  955. const {
  956. parentSchema
  957. } = error;
  958. return `${instancePath} should be equal to constant ${this.getSchemaPartText(parentSchema, false, true)}`;
  959. }
  960. case "not":
  961. {
  962. const postfix = likeObject(
  963. /** @type {Schema} */
  964. error.parentSchema) ? `\n${this.getSchemaPartText(error.parentSchema)}` : "";
  965. const schemaOutput = this.getSchemaPartText(error.schema, false, false, false);
  966. if (canApplyNot(error.schema)) {
  967. return `${instancePath} should be any ${schemaOutput}${postfix}.`;
  968. }
  969. const {
  970. schema,
  971. parentSchema
  972. } = error;
  973. return `${instancePath} should not be ${this.getSchemaPartText(schema, false, true)}${parentSchema && likeObject(parentSchema) ? `\n${this.getSchemaPartText(parentSchema)}` : ""}`;
  974. }
  975. case "oneOf":
  976. case "anyOf":
  977. {
  978. const {
  979. parentSchema,
  980. children
  981. } = error;
  982. if (children && children.length > 0) {
  983. if (error.schema.length === 1) {
  984. const lastChild = children[children.length - 1];
  985. const remainingChildren = children.slice(0, children.length - 1);
  986. return this.formatValidationError(Object.assign({}, lastChild, {
  987. children: remainingChildren,
  988. parentSchema: Object.assign({}, parentSchema, lastChild.parentSchema)
  989. }));
  990. }
  991. let filteredChildren = filterChildren(children);
  992. if (filteredChildren.length === 1) {
  993. return this.formatValidationError(filteredChildren[0]);
  994. }
  995. filteredChildren = groupChildrenByFirstChild(filteredChildren);
  996. return `${instancePath} should be one of these:\n${this.getSchemaPartText(parentSchema)}\nDetails:\n${filteredChildren.map(
  997. /**
  998. * @param {SchemaUtilErrorObject} nestedError
  999. * @returns {string}
  1000. */
  1001. nestedError => ` * ${indent(this.formatValidationError(nestedError), " ")}`).join("\n")}`;
  1002. }
  1003. return `${instancePath} should be one of these:\n${this.getSchemaPartText(parentSchema)}`;
  1004. }
  1005. case "if":
  1006. {
  1007. const {
  1008. params,
  1009. parentSchema
  1010. } = error;
  1011. const {
  1012. failingKeyword
  1013. } = params;
  1014. return `${instancePath} should match "${failingKeyword}" schema:\n${this.getSchemaPartText(parentSchema, [failingKeyword])}`;
  1015. }
  1016. case "absolutePath":
  1017. {
  1018. const {
  1019. message,
  1020. parentSchema
  1021. } = error;
  1022. return `${instancePath}: ${message}${this.getSchemaPartDescription(parentSchema)}`;
  1023. }
  1024. /* istanbul ignore next */
  1025. default:
  1026. {
  1027. const {
  1028. message,
  1029. parentSchema
  1030. } = error;
  1031. const ErrorInJSON = JSON.stringify(error, null, 2); // For `custom`, `false schema`, `$ref` keywords
  1032. // Fallback for unknown keywords
  1033. return `${instancePath} ${message} (${ErrorInJSON}).\n${this.getSchemaPartText(parentSchema, false)}`;
  1034. }
  1035. }
  1036. }
  1037. /**
  1038. * @param {Array<SchemaUtilErrorObject>} errors
  1039. * @returns {string}
  1040. */
  1041. formatValidationErrors(errors) {
  1042. return errors.map(error => {
  1043. let formattedError = this.formatValidationError(error);
  1044. if (this.postFormatter) {
  1045. formattedError = this.postFormatter(formattedError, error);
  1046. }
  1047. return ` - ${indent(formattedError, " ")}`;
  1048. }).join("\n");
  1049. }
  1050. }
  1051. var _default = ValidationError;
  1052. exports.default = _default;