ValidationError.js 36 KB

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