ValidationError.js 35 KB

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