ValidationError.js 35 KB

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