custom.spec.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  1. 'use strict';
  2. var getAjvInstances = require('./ajv_instances')
  3. , should = require('./chai').should()
  4. , equal = require('../lib/compile/equal')
  5. , customRules = require('./custom_rules');
  6. describe('Custom keywords', function () {
  7. var ajv, instances;
  8. beforeEach(function() {
  9. instances = getAjvInstances({
  10. allErrors: true,
  11. verbose: true,
  12. inlineRefs: false
  13. });
  14. ajv = instances[0];
  15. });
  16. describe('custom rules', function() {
  17. describe('rule with "interpreted" keyword validation', function() {
  18. it('should add and validate rule', function() {
  19. testEvenKeyword({ type: 'number', validate: validateEven });
  20. function validateEven(schema, data) {
  21. if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
  22. return data % 2 ? !schema : schema;
  23. }
  24. });
  25. it('should add, validate keyword schema and validate rule', function() {
  26. testEvenKeyword({
  27. type: 'number',
  28. validate: validateEven,
  29. metaSchema: { "type": "boolean" }
  30. });
  31. shouldBeInvalidSchema({ "x-even": "not_boolean" });
  32. function validateEven(schema, data) {
  33. return data % 2 ? !schema : schema;
  34. }
  35. });
  36. it('should pass parent schema to "interpreted" keyword validation', function() {
  37. testRangeKeyword({
  38. type: 'number',
  39. validate: validateRange
  40. });
  41. function validateRange(schema, data, parentSchema) {
  42. validateRangeSchema(schema, parentSchema);
  43. return parentSchema.exclusiveRange === true
  44. ? data > schema[0] && data < schema[1]
  45. : data >= schema[0] && data <= schema[1];
  46. }
  47. });
  48. it('should validate meta schema and pass parent schema to "interpreted" keyword validation', function() {
  49. testRangeKeyword({
  50. type: 'number',
  51. validate: validateRange,
  52. metaSchema: {
  53. "type": "array",
  54. "items": [ { "type": "number" }, { "type": "number" } ],
  55. "additionalItems": false
  56. }
  57. });
  58. shouldBeInvalidSchema({ 'x-range': [ "1", 2 ] });
  59. shouldBeInvalidSchema({ 'x-range': {} });
  60. shouldBeInvalidSchema({ 'x-range': [ 1, 2, 3 ] });
  61. function validateRange(schema, data, parentSchema) {
  62. return parentSchema.exclusiveRange === true
  63. ? data > schema[0] && data < schema[1]
  64. : data >= schema[0] && data <= schema[1];
  65. }
  66. });
  67. it('should allow defining custom errors for "interpreted" keyword', function() {
  68. testRangeKeyword({ type: 'number', validate: validateRange }, true);
  69. function validateRange(schema, data, parentSchema) {
  70. validateRangeSchema(schema, parentSchema);
  71. var min = schema[0]
  72. , max = schema[1]
  73. , exclusive = parentSchema.exclusiveRange === true;
  74. var minOk = exclusive ? data > min : data >= min;
  75. var maxOk = exclusive ? data < max : data <= max;
  76. var valid = minOk && maxOk;
  77. if (!valid) {
  78. var err = { keyword: 'x-range' };
  79. validateRange.errors = [err];
  80. var comparison, limit;
  81. if (minOk) {
  82. comparison = exclusive ? '<' : '<=';
  83. limit = max;
  84. } else {
  85. comparison = exclusive ? '>' : '>=';
  86. limit = min;
  87. }
  88. err.message = 'should be ' + comparison + ' ' + limit;
  89. err.params = {
  90. comparison: comparison,
  91. limit: limit,
  92. exclusive: exclusive
  93. };
  94. }
  95. return valid;
  96. }
  97. });
  98. });
  99. describe('rule with "compiled" keyword validation', function() {
  100. it('should add and validate rule', function() {
  101. testEvenKeyword({ type: 'number', compile: compileEven });
  102. shouldBeInvalidSchema({ "x-even": "not_boolean" });
  103. function compileEven(schema) {
  104. if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
  105. return schema ? isEven : isOdd;
  106. }
  107. function isEven(data) { return data % 2 === 0; }
  108. function isOdd(data) { return data % 2 !== 0; }
  109. });
  110. it('should add, validate keyword schema and validate rule', function() {
  111. testEvenKeyword({
  112. type: 'number',
  113. compile: compileEven,
  114. metaSchema: { "type": "boolean" }
  115. });
  116. shouldBeInvalidSchema({ "x-even": "not_boolean" });
  117. function compileEven(schema) {
  118. return schema ? isEven : isOdd;
  119. }
  120. function isEven(data) { return data % 2 === 0; }
  121. function isOdd(data) { return data % 2 !== 0; }
  122. });
  123. it('should compile keyword validating function only once per schema', function () {
  124. testConstantKeyword({ compile: compileConstant });
  125. });
  126. it('should allow multiple schemas for the same keyword', function () {
  127. testMultipleConstantKeyword({ compile: compileConstant });
  128. });
  129. it('should pass parent schema to "compiled" keyword validation', function() {
  130. testRangeKeyword({ type: 'number', compile: compileRange });
  131. });
  132. it('should allow multiple parent schemas for the same keyword', function () {
  133. testMultipleRangeKeyword({ type: 'number', compile: compileRange });
  134. });
  135. });
  136. function compileConstant(schema) {
  137. return typeof schema == 'object' && schema !== null
  138. ? isDeepEqual
  139. : isStrictEqual;
  140. function isDeepEqual(data) { return equal(data, schema); }
  141. function isStrictEqual(data) { return data === schema; }
  142. }
  143. function compileRange(schema, parentSchema) {
  144. validateRangeSchema(schema, parentSchema);
  145. var min = schema[0];
  146. var max = schema[1];
  147. return parentSchema.exclusiveRange === true
  148. ? function (data) { return data > min && data < max; }
  149. : function (data) { return data >= min && data <= max; };
  150. }
  151. });
  152. describe('macro rules', function() {
  153. it('should add and validate rule with "macro" keyword', function() {
  154. testEvenKeyword({ type: 'number', macro: macroEven }, 2);
  155. });
  156. it('should add and expand macro rule', function() {
  157. testConstantKeyword({ macro: macroConstant }, 2);
  158. });
  159. it('should allow multiple schemas for the same macro keyword', function () {
  160. testMultipleConstantKeyword({ macro: macroConstant }, 2);
  161. });
  162. it('should pass parent schema to "macro" keyword', function() {
  163. testRangeKeyword({ type: 'number', macro: macroRange }, undefined, 2);
  164. });
  165. it('should allow multiple parent schemas for the same macro keyword', function () {
  166. testMultipleRangeKeyword({ type: 'number', macro: macroRange }, 2);
  167. });
  168. it('should recursively expand macro keywords', function() {
  169. instances.forEach(function (_ajv) {
  170. _ajv.addKeyword('deepProperties', { type: 'object', macro: macroDeepProperties });
  171. _ajv.addKeyword('range', { type: 'number', macro: macroRange });
  172. var schema = {
  173. "deepProperties": {
  174. "a.b.c": { "type": "number", "range": [2,4] },
  175. "d.e.f.g": { "type": "string" }
  176. }
  177. };
  178. /* This schema recursively expands to:
  179. {
  180. "allOf": [
  181. {
  182. "properties": {
  183. "a": {
  184. "properties": {
  185. "b": {
  186. "properties": {
  187. "c": {
  188. "type": "number",
  189. "minimum": 2,
  190. "exclusiveMinimum": false,
  191. "maximum": 4,
  192. "exclusiveMaximum": false
  193. }
  194. }
  195. }
  196. }
  197. }
  198. }
  199. },
  200. {
  201. "properties": {
  202. "d": {
  203. "properties": {
  204. "e": {
  205. "properties": {
  206. "f": {
  207. "properties": {
  208. "g": {
  209. "type": "string"
  210. }
  211. }
  212. }
  213. }
  214. }
  215. }
  216. }
  217. }
  218. }
  219. ]
  220. }
  221. */
  222. var validate = _ajv.compile(schema);
  223. shouldBeValid(validate, {
  224. a: {b: {c: 3}},
  225. d: {e: {f: {g: 'foo'}}}
  226. });
  227. shouldBeInvalid(validate, {
  228. a: {b: {c: 5}}, // out of range
  229. d: {e: {f: {g: 'foo'}}}
  230. }, 5);
  231. shouldBeInvalid(validate, {
  232. a: {b: {c: 'bar'}}, // not number
  233. d: {e: {f: {g: 'foo'}}}
  234. }, 4);
  235. shouldBeInvalid(validate, {
  236. a: {b: {c: 3}},
  237. d: {e: {f: {g: 2}}} // not string
  238. }, 5);
  239. function macroDeepProperties(_schema) {
  240. if (typeof _schema != 'object')
  241. throw new Error('schema of deepProperty should be an object');
  242. var expanded = [];
  243. for (var prop in _schema) {
  244. var path = prop.split('.');
  245. var properties = {};
  246. if (path.length == 1) {
  247. properties[prop] = _schema[prop];
  248. } else {
  249. var deepProperties = {};
  250. deepProperties[path.slice(1).join('.')] = _schema[prop];
  251. properties[path[0]] = { "deepProperties": deepProperties };
  252. }
  253. expanded.push({ "properties": properties });
  254. }
  255. return expanded.length == 1 ? expanded[0] : { "allOf": expanded };
  256. }
  257. });
  258. });
  259. it('should correctly expand multiple macros on the same level', function() {
  260. instances.forEach(function (_ajv) {
  261. _ajv.addKeyword('range', { type: 'number', macro: macroRange });
  262. _ajv.addKeyword('even', { type: 'number', macro: macroEven });
  263. var schema = {
  264. "range": [4,6],
  265. "even": true
  266. };
  267. var validate = _ajv.compile(schema);
  268. var numErrors = _ajv._opts.allErrors ? 4 : 2;
  269. shouldBeInvalid(validate, 2, 2);
  270. shouldBeInvalid(validate, 3, numErrors);
  271. shouldBeValid(validate, 4);
  272. shouldBeInvalid(validate, 5, 2);
  273. shouldBeValid(validate, 6);
  274. shouldBeInvalid(validate, 7, numErrors);
  275. shouldBeInvalid(validate, 8, 2);
  276. });
  277. });
  278. it('should validate macro keyword when it resolves to the same keyword as exists', function() {
  279. instances.forEach(function (_ajv) {
  280. _ajv.addKeyword('range', { type: 'number', macro: macroRange });
  281. var schema = {
  282. "range": [1,4],
  283. "minimum": 2.5
  284. };
  285. var validate = _ajv.compile(schema);
  286. shouldBeValid(validate, 3);
  287. shouldBeInvalid(validate, 2);
  288. });
  289. });
  290. it('should correctly expand macros in subschemas', function() {
  291. instances.forEach(function (_ajv) {
  292. _ajv.addKeyword('range', { type: 'number', macro: macroRange });
  293. var schema = {
  294. "allOf": [
  295. { "range": [4,8] },
  296. { "range": [2,6] }
  297. ]
  298. };
  299. var validate = _ajv.compile(schema);
  300. shouldBeInvalid(validate, 2, 2);
  301. shouldBeInvalid(validate, 3, 2);
  302. shouldBeValid(validate, 4);
  303. shouldBeValid(validate, 5);
  304. shouldBeValid(validate, 6);
  305. shouldBeInvalid(validate, 7, 2);
  306. shouldBeInvalid(validate, 8, 2);
  307. });
  308. });
  309. it('should correctly expand macros in macro expansions', function() {
  310. instances.forEach(function (_ajv) {
  311. _ajv.addKeyword('range', { type: 'number', macro: macroRange });
  312. _ajv.addKeyword('myContains', { type: 'array', macro: macroContains });
  313. var schema = {
  314. "myContains": {
  315. "type": "number",
  316. "range": [4,7],
  317. "exclusiveRange": true
  318. }
  319. };
  320. var validate = _ajv.compile(schema);
  321. shouldBeInvalid(validate, [1,2,3], 2);
  322. shouldBeInvalid(validate, [2,3,4], 2);
  323. shouldBeValid(validate, [3,4,5]); // only 5 is in range
  324. shouldBeValid(validate, [6,7,8]); // only 6 is in range
  325. shouldBeInvalid(validate, [7,8,9], 2);
  326. shouldBeInvalid(validate, [8,9,10], 2);
  327. function macroContains(_schema) {
  328. return { "not": { "items": { "not": _schema } } };
  329. }
  330. });
  331. });
  332. it('should throw exception if macro expansion is an invalid schema', function() {
  333. ajv.addKeyword('invalid', { macro: macroInvalid });
  334. var schema = { "invalid": true };
  335. should.throw(function() {
  336. ajv.compile(schema);
  337. });
  338. function macroInvalid(/* schema */) {
  339. return { "type": "invalid" };
  340. }
  341. });
  342. function macroEven(schema) {
  343. if (schema === true) return { "multipleOf": 2 };
  344. if (schema === false) return { "not": { "multipleOf": 2 } };
  345. throw new Error('Schema for "even" keyword should be boolean');
  346. }
  347. function macroConstant(schema/*, parentSchema */) {
  348. return { "enum": [schema] };
  349. }
  350. function macroRange(schema, parentSchema) {
  351. validateRangeSchema(schema, parentSchema);
  352. var exclusive = !!parentSchema.exclusiveRange;
  353. return exclusive
  354. ? { exclusiveMinimum: schema[0], exclusiveMaximum: schema[1] }
  355. : { minimum: schema[0], maximum: schema[1] };
  356. }
  357. });
  358. describe('inline rules', function() {
  359. it('should add and validate rule with "inline" code keyword', function() {
  360. testEvenKeyword({ type: 'number', inline: inlineEven });
  361. });
  362. it('should pass parent schema to "inline" keyword', function() {
  363. testRangeKeyword({ type: 'number', inline: inlineRange, statements: true });
  364. });
  365. it('should define "inline" keyword as template', function() {
  366. var inlineRangeTemplate = customRules.range;
  367. testRangeKeyword({
  368. type: 'number',
  369. inline: inlineRangeTemplate,
  370. statements: true
  371. });
  372. });
  373. it('should define "inline" keyword without errors', function() {
  374. var inlineRangeTemplate = customRules.range;
  375. testRangeKeyword({
  376. type: 'number',
  377. inline: inlineRangeTemplate,
  378. statements: true,
  379. errors: false
  380. });
  381. });
  382. it('should allow defining optional errors', function() {
  383. var inlineRangeTemplate = customRules.rangeWithErrors;
  384. testRangeKeyword({
  385. type: 'number',
  386. inline: inlineRangeTemplate,
  387. statements: true
  388. }, true);
  389. });
  390. it('should allow defining required errors', function() {
  391. var inlineRangeTemplate = customRules.rangeWithErrors;
  392. testRangeKeyword({
  393. type: 'number',
  394. inline: inlineRangeTemplate,
  395. statements: true,
  396. errors: true
  397. }, true);
  398. });
  399. function inlineEven(it, keyword, schema) {
  400. var op = schema ? '===' : '!==';
  401. return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
  402. }
  403. function inlineRange(it, keyword, schema, parentSchema) {
  404. var min = schema[0]
  405. , max = schema[1]
  406. , data = 'data' + (it.dataLevel || '')
  407. , gt = parentSchema.exclusiveRange ? ' > ' : ' >= '
  408. , lt = parentSchema.exclusiveRange ? ' < ' : ' <= ';
  409. return 'var valid' + it.level + ' = ' + data + gt + min + ' && ' + data + lt + max + ';';
  410. }
  411. });
  412. describe('$data reference support with custom keywords (with $data option)', function() {
  413. beforeEach(function() {
  414. instances = getAjvInstances({
  415. allErrors: true,
  416. verbose: true,
  417. inlineRefs: false
  418. }, { $data: true });
  419. ajv = instances[0];
  420. });
  421. it('should validate "interpreted" rule', function() {
  422. testEvenKeyword$data({
  423. type: 'number',
  424. $data: true,
  425. validate: validateEven
  426. });
  427. function validateEven(schema, data) {
  428. if (typeof schema != 'boolean') return false;
  429. return data % 2 ? !schema : schema;
  430. }
  431. });
  432. it('should validate rule with "compile" and "validate" funcs', function() {
  433. var compileCalled;
  434. testEvenKeyword$data({
  435. type: 'number',
  436. $data: true,
  437. compile: compileEven,
  438. validate: validateEven
  439. });
  440. compileCalled .should.equal(true);
  441. function validateEven(schema, data) {
  442. if (typeof schema != 'boolean') return false;
  443. return data % 2 ? !schema : schema;
  444. }
  445. function compileEven(schema) {
  446. compileCalled = true;
  447. if (typeof schema != 'boolean') throw new Error('The value of "even" keyword must be boolean');
  448. return schema ? isEven : isOdd;
  449. }
  450. function isEven(data) { return data % 2 === 0; }
  451. function isOdd(data) { return data % 2 !== 0; }
  452. });
  453. it('should validate with "compile" and "validate" funcs with meta-schema', function() {
  454. var compileCalled;
  455. testEvenKeyword$data({
  456. type: 'number',
  457. $data: true,
  458. compile: compileEven,
  459. validate: validateEven,
  460. metaSchema: { "type": "boolean" }
  461. });
  462. compileCalled .should.equal(true);
  463. shouldBeInvalidSchema({ "x-even-$data": "false" });
  464. function validateEven(schema, data) {
  465. return data % 2 ? !schema : schema;
  466. }
  467. function compileEven(schema) {
  468. compileCalled = true;
  469. return schema ? isEven : isOdd;
  470. }
  471. function isEven(data) { return data % 2 === 0; }
  472. function isOdd(data) { return data % 2 !== 0; }
  473. });
  474. it('should validate rule with "macro" and "validate" funcs', function() {
  475. var macroCalled;
  476. testEvenKeyword$data({
  477. type: 'number',
  478. $data: true,
  479. macro: macroEven,
  480. validate: validateEven
  481. }, 2);
  482. macroCalled .should.equal(true);
  483. function validateEven(schema, data) {
  484. if (typeof schema != 'boolean') return false;
  485. return data % 2 ? !schema : schema;
  486. }
  487. function macroEven(schema) {
  488. macroCalled = true;
  489. if (schema === true) return { "multipleOf": 2 };
  490. if (schema === false) return { "not": { "multipleOf": 2 } };
  491. throw new Error('Schema for "even" keyword should be boolean');
  492. }
  493. });
  494. it('should validate with "macro" and "validate" funcs with meta-schema', function() {
  495. var macroCalled;
  496. testEvenKeyword$data({
  497. type: 'number',
  498. $data: true,
  499. macro: macroEven,
  500. validate: validateEven,
  501. metaSchema: { "type": "boolean" }
  502. }, 2);
  503. macroCalled .should.equal(true);
  504. shouldBeInvalidSchema({ "x-even-$data": "false" });
  505. function validateEven(schema, data) {
  506. return data % 2 ? !schema : schema;
  507. }
  508. function macroEven(schema) {
  509. macroCalled = true;
  510. if (schema === true) return { "multipleOf": 2 };
  511. if (schema === false) return { "not": { "multipleOf": 2 } };
  512. }
  513. });
  514. it('should validate rule with "inline" and "validate" funcs', function() {
  515. var inlineCalled;
  516. testEvenKeyword$data({
  517. type: 'number',
  518. $data: true,
  519. inline: inlineEven,
  520. validate: validateEven
  521. });
  522. inlineCalled .should.equal(true);
  523. function validateEven(schema, data) {
  524. if (typeof schema != 'boolean') return false;
  525. return data % 2 ? !schema : schema;
  526. }
  527. function inlineEven(it, keyword, schema) {
  528. inlineCalled = true;
  529. var op = schema ? '===' : '!==';
  530. return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
  531. }
  532. });
  533. it('should validate with "inline" and "validate" funcs with meta-schema', function() {
  534. var inlineCalled;
  535. testEvenKeyword$data({
  536. type: 'number',
  537. $data: true,
  538. inline: inlineEven,
  539. validate: validateEven,
  540. metaSchema: { "type": "boolean" }
  541. });
  542. inlineCalled .should.equal(true);
  543. shouldBeInvalidSchema({ "x-even-$data": "false" });
  544. function validateEven(schema, data) {
  545. return data % 2 ? !schema : schema;
  546. }
  547. function inlineEven(it, keyword, schema) {
  548. inlineCalled = true;
  549. var op = schema ? '===' : '!==';
  550. return 'data' + (it.dataLevel || '') + ' % 2 ' + op + ' 0';
  551. }
  552. });
  553. it('should fail if keyword definition has "$data" but no "validate"', function() {
  554. should.throw(function() {
  555. ajv.addKeyword('even', {
  556. type: 'number',
  557. $data: true,
  558. macro: function() { return {}; }
  559. });
  560. });
  561. });
  562. });
  563. function testEvenKeyword(definition, numErrors) {
  564. instances.forEach(function (_ajv) {
  565. _ajv.addKeyword('x-even', definition);
  566. var schema = { "x-even": true };
  567. var validate = _ajv.compile(schema);
  568. shouldBeValid(validate, 2);
  569. shouldBeValid(validate, 'abc');
  570. shouldBeInvalid(validate, 2.5, numErrors);
  571. shouldBeInvalid(validate, 3, numErrors);
  572. });
  573. }
  574. function testEvenKeyword$data(definition, numErrors) {
  575. instances.forEach(function (_ajv) {
  576. _ajv.addKeyword('x-even-$data', definition);
  577. var schema = { "x-even-$data": true };
  578. var validate = _ajv.compile(schema);
  579. shouldBeValid(validate, 2);
  580. shouldBeValid(validate, 'abc');
  581. shouldBeInvalid(validate, 2.5, numErrors);
  582. shouldBeInvalid(validate, 3, numErrors);
  583. schema = {
  584. "properties": {
  585. "data": { "x-even-$data": { "$data": "1/evenValue" } },
  586. "evenValue": {}
  587. }
  588. };
  589. validate = _ajv.compile(schema);
  590. shouldBeValid(validate, { data: 2, evenValue: true });
  591. shouldBeInvalid(validate, { data: 2, evenValue: false });
  592. shouldBeValid(validate, { data: 'abc', evenValue: true });
  593. shouldBeValid(validate, { data: 'abc', evenValue: false });
  594. shouldBeInvalid(validate, { data: 2.5, evenValue: true });
  595. shouldBeValid(validate, { data: 2.5, evenValue: false });
  596. shouldBeInvalid(validate, { data: 3, evenValue: true });
  597. shouldBeValid(validate, { data: 3, evenValue: false });
  598. shouldBeInvalid(validate, { data: 2, evenValue: "true" });
  599. // valid if the value of x-even-$data keyword is undefined
  600. shouldBeValid(validate, { data: 2 });
  601. shouldBeValid(validate, { data: 3 });
  602. });
  603. }
  604. function testConstantKeyword(definition, numErrors) {
  605. instances.forEach(function (_ajv) {
  606. _ajv.addKeyword('myConstant', definition);
  607. var schema = { "myConstant": "abc" };
  608. var validate = _ajv.compile(schema);
  609. shouldBeValid(validate, 'abc');
  610. shouldBeInvalid(validate, 2, numErrors);
  611. shouldBeInvalid(validate, {}, numErrors);
  612. });
  613. }
  614. function testMultipleConstantKeyword(definition, numErrors) {
  615. instances.forEach(function (_ajv) {
  616. _ajv.addKeyword('x-constant', definition);
  617. var schema = {
  618. "properties": {
  619. "a": { "x-constant": 1 },
  620. "b": { "x-constant": 1 }
  621. },
  622. "additionalProperties": { "x-constant": { "foo": "bar" } },
  623. "items": { "x-constant": { "foo": "bar" } }
  624. };
  625. var validate = _ajv.compile(schema);
  626. shouldBeValid(validate, {a:1, b:1});
  627. shouldBeInvalid(validate, {a:2, b:1}, numErrors);
  628. shouldBeValid(validate, {a:1, c: {foo: 'bar'}});
  629. shouldBeInvalid(validate, {a:1, c: {foo: 'baz'}}, numErrors);
  630. shouldBeValid(validate, [{foo: 'bar'}]);
  631. shouldBeValid(validate, [{foo: 'bar'}, {foo: 'bar'}]);
  632. shouldBeInvalid(validate, [1], numErrors);
  633. });
  634. }
  635. function testRangeKeyword(definition, customErrors, numErrors) {
  636. instances.forEach(function (_ajv) {
  637. _ajv.addKeyword('x-range', definition);
  638. var schema = { "x-range": [2, 4] };
  639. var validate = _ajv.compile(schema);
  640. shouldBeValid(validate, 2);
  641. shouldBeValid(validate, 3);
  642. shouldBeValid(validate, 4);
  643. shouldBeValid(validate, 'abc');
  644. shouldBeInvalid(validate, 1.99, numErrors);
  645. if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range', '>=', 2);
  646. shouldBeInvalid(validate, 4.01, numErrors);
  647. if (customErrors) shouldBeRangeError(validate.errors[0], '', '#/x-range','<=', 4);
  648. schema = {
  649. "properties": {
  650. "foo": {
  651. "x-range": [2, 4],
  652. "exclusiveRange": true
  653. }
  654. }
  655. };
  656. validate = _ajv.compile(schema);
  657. shouldBeValid(validate, { foo: 2.01 });
  658. shouldBeValid(validate, { foo: 3 });
  659. shouldBeValid(validate, { foo: 3.99 });
  660. shouldBeInvalid(validate, { foo: 2 }, numErrors);
  661. if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '>', 2, true);
  662. shouldBeInvalid(validate, { foo: 4 }, numErrors);
  663. if (customErrors) shouldBeRangeError(validate.errors[0], '.foo', '#/properties/foo/x-range', '<', 4, true);
  664. });
  665. }
  666. function testMultipleRangeKeyword(definition, numErrors) {
  667. instances.forEach(function (_ajv) {
  668. _ajv.addKeyword('x-range', definition);
  669. var schema = {
  670. "properties": {
  671. "a": { "x-range": [2, 4], "exclusiveRange": true },
  672. "b": { "x-range": [2, 4], "exclusiveRange": false }
  673. },
  674. "additionalProperties": { "x-range": [5, 7] },
  675. "items": { "x-range": [5, 7] }
  676. };
  677. var validate = _ajv.compile(schema);
  678. shouldBeValid(validate, {a:3.99, b:4});
  679. shouldBeInvalid(validate, {a:4, b:4}, numErrors);
  680. shouldBeValid(validate, {a:2.01, c: 7});
  681. shouldBeInvalid(validate, {a:2.01, c: 7.01}, numErrors);
  682. shouldBeValid(validate, [5, 6, 7]);
  683. shouldBeInvalid(validate, [7.01], numErrors);
  684. });
  685. }
  686. function shouldBeRangeError(error, dataPath, schemaPath, comparison, limit, exclusive) {
  687. delete error.schema;
  688. delete error.data;
  689. error .should.eql({
  690. keyword: 'x-range',
  691. dataPath: dataPath,
  692. schemaPath: schemaPath,
  693. message: 'should be ' + comparison + ' ' + limit,
  694. params: {
  695. comparison: comparison,
  696. limit: limit,
  697. exclusive: !!exclusive
  698. }
  699. });
  700. }
  701. function validateRangeSchema(schema, parentSchema) {
  702. var schemaValid = Array.isArray(schema) && schema.length == 2
  703. && typeof schema[0] == 'number'
  704. && typeof schema[1] == 'number';
  705. if (!schemaValid) throw new Error('Invalid schema for range keyword, should be array of 2 numbers');
  706. var exclusiveRangeSchemaValid = parentSchema.exclusiveRange === undefined
  707. || typeof parentSchema.exclusiveRange == 'boolean';
  708. if (!exclusiveRangeSchemaValid) throw new Error('Invalid schema for exclusiveRange keyword, should be bolean');
  709. }
  710. function shouldBeValid(validate, data) {
  711. validate(data) .should.equal(true);
  712. should.not.exist(validate.errors);
  713. }
  714. function shouldBeInvalid(validate, data, numErrors) {
  715. validate(data) .should.equal(false);
  716. validate.errors .should.have.length(numErrors || 1);
  717. }
  718. function shouldBeInvalidSchema(schema) {
  719. instances.forEach(function (_ajv) {
  720. should.throw(function() {
  721. _ajv.compile(schema);
  722. });
  723. });
  724. }
  725. describe('addKeyword method', function() {
  726. var TEST_TYPES = [ undefined, 'number', 'string', 'boolean', ['number', 'string']];
  727. it('should throw if defined keyword is passed', function() {
  728. testThrow(['minimum', 'maximum', 'multipleOf', 'minLength', 'maxLength']);
  729. testThrowDuplicate('custom');
  730. function testThrow(keywords) {
  731. TEST_TYPES.forEach(function (dataType, index) {
  732. should.throw(function(){
  733. addKeyword(keywords[index], dataType);
  734. });
  735. });
  736. }
  737. function testThrowDuplicate(keywordPrefix) {
  738. var index = 0;
  739. TEST_TYPES.forEach(function (dataType1) {
  740. TEST_TYPES.forEach(function (dataType2) {
  741. var keyword = keywordPrefix + (index++);
  742. addKeyword(keyword, dataType1);
  743. should.throw(function() {
  744. addKeyword(keyword, dataType2);
  745. });
  746. });
  747. });
  748. }
  749. });
  750. it('should throw if keyword is not a valid name', function() {
  751. should.not.throw(function() {
  752. ajv.addKeyword('mykeyword', {
  753. validate: function() { return true; }
  754. });
  755. });
  756. should.not.throw(function() {
  757. ajv.addKeyword('hyphens-are-valid', {
  758. validate: function() { return true; }
  759. });
  760. });
  761. should.throw(function() {
  762. ajv.addKeyword('3-start-with-number-not-valid`', {
  763. validate: function() { return true; }
  764. });
  765. });
  766. should.throw(function() {
  767. ajv.addKeyword('-start-with-hyphen-not-valid`', {
  768. validate: function() { return true; }
  769. });
  770. });
  771. should.throw(function() {
  772. ajv.addKeyword('spaces not valid`', {
  773. validate: function() { return true; }
  774. });
  775. });
  776. });
  777. it('should return instance of itself', function() {
  778. var res = ajv.addKeyword('any', {
  779. validate: function() { return true; }
  780. });
  781. res.should.equal(ajv);
  782. });
  783. it('should throw if unknown type is passed', function() {
  784. should.throw(function() {
  785. addKeyword('custom1', 'wrongtype');
  786. });
  787. should.throw(function() {
  788. addKeyword('custom2', ['number', 'wrongtype']);
  789. });
  790. should.throw(function() {
  791. addKeyword('custom3', ['number', undefined]);
  792. });
  793. });
  794. function addKeyword(keyword, dataType) {
  795. ajv.addKeyword(keyword, {
  796. type: dataType,
  797. validate: function() {}
  798. });
  799. }
  800. });
  801. describe('getKeyword', function() {
  802. it('should return boolean for pre-defined and unknown keywords', function() {
  803. ajv.getKeyword('type') .should.equal(true);
  804. ajv.getKeyword('properties') .should.equal(true);
  805. ajv.getKeyword('additionalProperties') .should.equal(true);
  806. ajv.getKeyword('unknown') .should.equal(false);
  807. });
  808. it('should return keyword definition for custom keywords', function() {
  809. var definition = {
  810. validate: function() { return true; }
  811. };
  812. ajv.addKeyword('mykeyword', definition);
  813. ajv.getKeyword('mykeyword') .should.equal(definition);
  814. });
  815. });
  816. describe('removeKeyword', function() {
  817. it('should remove and allow redefining custom keyword', function() {
  818. ajv.addKeyword('positive', {
  819. type: 'number',
  820. validate: function (schema, data) { return data > 0; }
  821. });
  822. var schema = { positive: true };
  823. var validate = ajv.compile(schema);
  824. validate(0) .should.equal(false);
  825. validate(1) .should.equal(true);
  826. should.throw(function() {
  827. ajv.addKeyword('positive', {
  828. type: 'number',
  829. validate: function(sch, data) { return data >= 0; }
  830. });
  831. });
  832. ajv.removeKeyword('positive');
  833. ajv.removeSchema(schema);
  834. ajv.addKeyword('positive', {
  835. type: 'number',
  836. validate: function (sch, data) { return data >= 0; }
  837. });
  838. validate = ajv.compile(schema);
  839. validate(-1) .should.equal(false);
  840. validate(0) .should.equal(true);
  841. validate(1) .should.equal(true);
  842. });
  843. it('should remove and allow redefining standard keyword', function() {
  844. var schema = { minimum: 1 };
  845. var validate = ajv.compile(schema);
  846. validate(0) .should.equal(false);
  847. validate(1) .should.equal(true);
  848. validate(2) .should.equal(true);
  849. ajv.removeKeyword('minimum');
  850. ajv.removeSchema(schema);
  851. validate = ajv.compile(schema);
  852. validate(0) .should.equal(true);
  853. validate(1) .should.equal(true);
  854. validate(2) .should.equal(true);
  855. ajv.addKeyword('minimum', {
  856. type: 'number',
  857. // make minimum exclusive
  858. validate: function (sch, data) { return data > sch; }
  859. });
  860. ajv.removeSchema(schema);
  861. validate = ajv.compile(schema);
  862. validate(0) .should.equal(false);
  863. validate(1) .should.equal(false);
  864. validate(2) .should.equal(true);
  865. });
  866. it('should return instance of itself', function() {
  867. var res = ajv
  868. .addKeyword('any', {
  869. validate: function() { return true; }
  870. })
  871. .removeKeyword('any');
  872. res.should.equal(ajv);
  873. });
  874. });
  875. describe('custom keywords mutating data', function() {
  876. it('should NOT update data without option modifying', function() {
  877. should.throw(function() {
  878. testModifying(false);
  879. });
  880. });
  881. it('should update data with option modifying', function() {
  882. testModifying(true);
  883. });
  884. function testModifying(withOption) {
  885. var collectionFormat = {
  886. csv: function (data, dataPath, parentData, parentDataProperty) {
  887. parentData[parentDataProperty] = data.split(',');
  888. return true;
  889. }
  890. };
  891. ajv.addKeyword('collectionFormat', {
  892. type: 'string',
  893. modifying: withOption,
  894. compile: function(schema) { return collectionFormat[schema]; },
  895. metaSchema: {
  896. enum: ['csv']
  897. }
  898. });
  899. var validate = ajv.compile({
  900. type: 'object',
  901. properties: {
  902. foo: {
  903. allOf: [
  904. { collectionFormat: 'csv' },
  905. {
  906. type: 'array',
  907. items: { type: 'string' },
  908. }
  909. ]
  910. }
  911. },
  912. additionalProperties: false
  913. });
  914. var obj = { foo: 'bar,baz,quux' };
  915. validate(obj) .should.equal(true);
  916. obj .should.eql({ foo: ['bar', 'baz', 'quux'] });
  917. }
  918. });
  919. describe('custom keywords with predefined validation result', function() {
  920. it('should ignore result from validation function', function() {
  921. ajv.addKeyword('pass', {
  922. validate: function() { return false; },
  923. valid: true
  924. });
  925. ajv.addKeyword('fail', {
  926. validate: function() { return true; },
  927. valid: false
  928. });
  929. ajv.validate({ pass: '' }, 1) .should.equal(true);
  930. ajv.validate({ fail: '' }, 1) .should.equal(false);
  931. });
  932. it('should throw exception if used with macro keyword', function() {
  933. should.throw(function() {
  934. ajv.addKeyword('pass', {
  935. macro: function() { return {}; },
  936. valid: true
  937. });
  938. });
  939. should.throw(function() {
  940. ajv.addKeyword('fail', {
  941. macro: function() { return {not:{}}; },
  942. valid: false
  943. });
  944. });
  945. });
  946. });
  947. });