import type { ErrorObject , SchemaObject , SchemaValidateFunction } from "../lib/types" ; import type AjvCore from "../dist/core" ; import { _ , nil } from "../dist/compile/codegen/code" ; import getAjvAllInstances from "./ajv_all_instances" ; import _Ajv from "./ajv" ; import equal from "../dist/runtime/equal" ; import assert = require ( "assert" ) ; import chai from "./chai" ; const should = chai . should ( ) ; describe ( "User-defined keywords" , ( ) => { let ajv , instances ; beforeEach ( ( ) => { instances = getAjvAllInstances ( { allErrors : true , verbose : true , inlineRefs : false , } , { allowUnionTypes : true } ) ; ajv = instances [ 0 ] ; } ) ; describe ( "user-defined keyword" , ( ) => { describe ( 'keyword with "validate" function' , ( ) => { it ( "should add and validate keyword" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , validate : validateEven } ) ; function validateEven ( schema , data ) { if ( typeof schema != "boolean" ) { throw new Error ( 'The value of "even" keyword must be boolean' ) ; } return data % 2 ? ! schema : schema ; } } ) ; it ( "should add, validate keyword schema and validate rule" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , validate : validateEven , metaSchema : { type : "boolean" } , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-even" : "not_boolean" } ) ; function validateEven ( schema , data ) { return data % 2 ? ! schema : schema ; } } ) ; it ( 'should pass parent schema to "interpreted" keyword validation' , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , validate : validateRange , } ) ; function validateRange ( schema , data , parentSchema ) { validateRangeSchema ( schema , parentSchema ) ; return parentSchema . exclusiveRange === true ? data > schema [ 0 ] && data < schema [ 1 ] : data >= schema [ 0 ] && data <= schema [ 1 ] ; } } ) ; it ( 'should validate meta schema and pass parent schema to "interpreted" keyword validation' , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , validate : validateRange , metaSchema : { type : "array" , items : [ { type : "number" } , { type : "number" } ] , minItems : 2 , additionalItems : false , } , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-range" : [ "1" , 2 ] } ) ; shouldBeInvalidSchema ( { type : "number" , "x-range" : { } } ) ; shouldBeInvalidSchema ( { type : "number" , "x-range" : [ 1 , 2 , 3 ] } ) ; function validateRange ( schema , data , parentSchema ) { return parentSchema . exclusiveRange === true ? data > schema [ 0 ] && data < schema [ 1 ] : data >= schema [ 0 ] && data <= schema [ 1 ] ; } } ) ; it ( 'should allow defining errors for "validate" keyword' , ( ) => { const validateRange = _validateRange ; testRangeKeyword ( { keyword : "x-range" , type : "number" , validate : validateRange } , true ) ; function _validateRange ( schema , data , parentSchema ) { validateRangeSchema ( schema , parentSchema ) ; const min = schema [ 0 ] , max = schema [ 1 ] , exclusive = parentSchema . exclusiveRange === true ; const minOk = exclusive ? data > min : data >= min ; const maxOk = exclusive ? data < max : data <= max ; const valid = minOk && maxOk ; if ( ! valid ) { const err = { keyword : "x-range" } ; validateRange . errors = [ err ] ; let comparison , limit ; if ( minOk ) { comparison = exclusive ? "<" : "<=" ; limit = max ; } else { comparison = exclusive ? ">" : ">=" ; limit = min ; } err . message = "should be " + comparison + " " + limit ; err . params = { comparison : comparison , limit : limit , exclusive : exclusive , } ; } return valid ; } } ) ; it ( "should support schemaType" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , schemaType : "boolean" , validate : ( schema , data ) => ( data % 2 ? ! schema : schema ) , } ) ; } ) ; } ) ; describe ( 'keyword with "compile" function' , ( ) => { it ( "should add and validate keyword" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , compile : compileEven , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-even" : "not_boolean" , } , 'The value of "x-even" keyword must be boolean' ) ; function compileEven ( schema ) { if ( typeof schema != "boolean" ) { throw new Error ( 'The value of "x-even" keyword must be boolean' ) ; } return schema ? isEven : isOdd ; } function isEven ( data ) { return data % 2 === 0 ; } function isOdd ( data ) { return data % 2 !== 0 ; } } ) ; it ( "should add, validate keyword schema and validate rule" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , compile : compileEven , metaSchema : { type : "boolean" } , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-even" : "not_boolean" , } ) ; function compileEven ( schema ) { return schema ? isEven : isOdd ; } function isEven ( data ) { return data % 2 === 0 ; } function isOdd ( data ) { return data % 2 !== 0 ; } } ) ; it ( "should compile keyword validating function only once per schema" , ( ) => { testConstantKeyword ( { keyword : "myConstant" , compile : compileConstant } ) ; } ) ; it ( "should allow multiple schemas for the same keyword" , ( ) => { testMultipleConstantKeyword ( { keyword : "x-constant" , compile : compileConstant } ) ; } ) ; it ( 'should pass parent schema to "compiled" keyword validation' , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , compile : compileRange } ) ; } ) ; it ( "should allow multiple parent schemas for the same keyword" , ( ) => { testMultipleRangeKeyword ( { keyword : "x-range" , type : "number" , compile : compileRange } ) ; } ) ; it ( "should support schemaType" , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , schemaType : "boolean" , compile : compileEven , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-even" : "not_boolean" , } , 'x-even value must be ["boolean"]' ) ; function compileEven ( schema ) { if ( schema ) return ( data ) => data % 2 === 0 ; return ( data ) => data % 2 !== 0 ; } } ) ; } ) ; function compileConstant ( schema ) { return typeof schema == "object" && schema !== null ? isDeepEqual : isStrictEqual ; function isDeepEqual ( data ) { return equal ( data , schema ) ; } function isStrictEqual ( data ) { return data === schema ; } } function compileRange ( schema , parentSchema ) { validateRangeSchema ( schema , parentSchema ) ; const min = schema [ 0 ] ; const max = schema [ 1 ] ; return parentSchema . exclusiveRange === true ? ( data ) => data > min && data < max : ( data ) => data >= min && data <= max ; } } ) ; describe ( "macro keywords" , ( ) => { it ( 'should add and validate keywords with "macro" function' , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , macro : macroEven } , 2 ) ; } ) ; it ( "should add and expand macro rule" , ( ) => { testConstantKeyword ( { keyword : "myConstant" , macro : macroConstant } , 2 ) ; } ) ; it ( "should allow multiple schemas for the same macro keyword" , ( ) => { testMultipleConstantKeyword ( { keyword : "x-constant" , macro : macroConstant } , 2 ) ; } ) ; it ( 'should pass parent schema to "macro" keyword' , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , macro : macroRange } , undefined , 2 ) ; } ) ; it ( "should allow multiple parent schemas for the same macro keyword" , ( ) => { testMultipleRangeKeyword ( { keyword : "x-range" , type : "number" , macro : macroRange } , 2 ) ; } ) ; it ( "should support resolving $ref without id or $id" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "macroRef" , macro ( schema , _parentSchema , it ) { it . baseId . should . equal ( "#" ) ; const ref = schema . $ref ; const validate = _ajv . getSchema ( ref ) ; if ( ! validate ) throw new _Ajv . MissingRefError ( it . baseId , ref ) ; return validate . schema ; } , metaSchema : { type : "object" , required : [ "$ref" ] , additionalProperties : false , properties : { $ref : { type : "string" , } , } , } , } ) ; const schema = { macroRef : { $ref : "#/definitions/schema" , } , definitions : { schema : { type : "string" , } , } , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , "foo" ) ; shouldBeInvalid ( validate , 1 , 2 ) ; } ) ; } ) ; it ( "should recursively expand macro keywords" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "deepProperties" , type : "object" , macro : macroDeepProperties , } ) ; _ajv . addKeyword ( { keyword : "range" , type : "number" , macro : macroRange } ) ; const schema = { type : "object" , deepProperties : { "a.b.c" : { type : "number" , range : [ 2 , 4 ] } , "d.e.f.g" : { type : "string" } , } , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , { a : { b : { c : 3 } } , d : { e : { f : { g : "foo" } } } , } ) ; shouldBeInvalid ( validate , { a : { b : { c : 5 } } , d : { e : { f : { g : "foo" } } } , } , 5 ) ; shouldBeInvalid ( validate , { a : { b : { c : "bar" } } , d : { e : { f : { g : "foo" } } } , } , 4 ) ; shouldBeInvalid ( validate , { a : { b : { c : 3 } } , d : { e : { f : { g : 2 } } } , } , 5 ) ; function macroDeepProperties ( _schema ) { if ( typeof _schema != "object" ) { throw new Error ( "schema of deepProperty should be an object" ) ; } const expanded = [ ] ; for ( const prop in _schema ) { const path = prop . split ( "." ) ; const properties = { } ; if ( path . length === 1 ) { properties [ prop ] = _schema [ prop ] ; } else { const deepProperties = { } ; deepProperties [ path . slice ( 1 ) . join ( "." ) ] = _schema [ prop ] ; properties [ path [ 0 ] ] = { type : "object" , deepProperties } ; } expanded . push ( { type : "object" , properties } ) ; } return expanded . length === 1 ? expanded [ 0 ] : { allOf : expanded } ; } } ) ; } ) ; it ( "should correctly expand multiple macros on the same level" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "range" , type : "number" , macro : macroRange } ) ; _ajv . addKeyword ( { keyword : "even" , type : "number" , macro : macroEven } ) ; const schema = { type : "number" , range : [ 4 , 6 ] , even : true , } ; const validate = _ajv . compile ( schema ) ; const numErrors = _ajv . opts . allErrors ? 4 : 2 ; shouldBeInvalid ( validate , 2 , 2 ) ; shouldBeInvalid ( validate , 3 , numErrors ) ; shouldBeValid ( validate , 4 ) ; shouldBeInvalid ( validate , 5 , 2 ) ; shouldBeValid ( validate , 6 ) ; shouldBeInvalid ( validate , 7 , numErrors ) ; shouldBeInvalid ( validate , 8 , 2 ) ; } ) ; } ) ; it ( "should validate macro keyword when it resolves to the same keyword as exists" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "range" , type : "number" , macro : macroRange } ) ; const schema = { type : "number" , range : [ 1 , 4 ] , minimum : 2.5 , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , 3 ) ; shouldBeInvalid ( validate , 2 ) ; } ) ; } ) ; it ( "should correctly expand macros in subschemas" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "range" , type : "number" , macro : macroRange } ) ; const schema = { type : "number" , allOf : [ { range : [ 4 , 8 ] } , { range : [ 2 , 6 ] } ] , } ; const validate = _ajv . compile ( schema ) ; shouldBeInvalid ( validate , 2 , 2 ) ; shouldBeInvalid ( validate , 3 , 2 ) ; shouldBeValid ( validate , 4 ) ; shouldBeValid ( validate , 5 ) ; shouldBeValid ( validate , 6 ) ; shouldBeInvalid ( validate , 7 , 2 ) ; shouldBeInvalid ( validate , 8 , 2 ) ; } ) ; } ) ; it ( "should correctly expand macros in macro expansions" , ( ) => { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( { keyword : "range" , type : "number" , macro : macroRange } ) ; _ajv . addKeyword ( { keyword : "exclusiveRange" , metaSchema : { type : "boolean" } } ) ; _ajv . addKeyword ( { keyword : "myContains" , type : "array" , macro : macroContains } ) ; const schema = { type : "array" , myContains : { type : "number" , range : [ 4 , 7 ] , exclusiveRange : true , } , } ; const validate = _ajv . compile ( schema ) ; shouldBeInvalid ( validate , [ 1 , 2 , 3 ] , 2 ) ; shouldBeInvalid ( validate , [ 2 , 3 , 4 ] , 2 ) ; shouldBeValid ( validate , [ 3 , 4 , 5 ] ) ; shouldBeValid ( validate , [ 6 , 7 , 8 ] ) ; shouldBeInvalid ( validate , [ 7 , 8 , 9 ] , 2 ) ; shouldBeInvalid ( validate , [ 8 , 9 , 10 ] , 2 ) ; function macroContains ( _schema ) { return { not : { items : { not : _schema } } } ; } } ) ; } ) ; it ( "should throw exception if macro expansion is an invalid schema" , ( ) => { ajv . addKeyword ( { keyword : "invalid" , macro : macroInvalid } ) ; const schema = { invalid : true } ; should . throw ( ( ) => { ajv . compile ( schema ) ; } , /type must be equal to one of the allowed values/ ) ; function macroInvalid ( ) { return { type : "invalid" } ; } } ) ; function macroEven ( schema ) { if ( schema === true ) return { multipleOf : 2 } ; if ( schema === false ) return { not : { multipleOf : 2 } } ; throw new Error ( 'Schema for "even" keyword should be boolean' ) ; } function macroConstant ( schema ) { return { enum : [ schema ] } ; } function macroRange ( schema , parentSchema ) { validateRangeSchema ( schema , parentSchema ) ; const exclusive = ! ! parentSchema . exclusiveRange ; return exclusive ? { exclusiveMinimum : schema [ 0 ] , exclusiveMaximum : schema [ 1 ] } : { minimum : schema [ 0 ] , maximum : schema [ 1 ] } ; } } ) ; describe ( '"code" keywords' , ( ) => { it ( 'should add and validate keyword with "code" function' , ( ) => { testEvenKeyword ( { keyword : "x-even" , type : "number" , code ( cxt ) { const { schema , data } = cxt ; const op = schema ? _ `===` : _ `!==` ; cxt . pass ( _ `${ data } % 2 ${ op } 0` ) ; } , } ) ; } ) ; it ( 'should pass parent schema to "inline" keyword' , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , code ( cxt ) { const { schema : [ min , max ] , parentSchema , data , } = cxt ; const eq = parentSchema . exclusiveRange ? nil : _ `=` ; cxt . pass ( _ `${ data } >${ eq } ${ min } && ${ data } <${ eq } ${ max }` ) ; } , } ) ; } ) ; it ( "should allow defining keyword error" , ( ) => { testRangeKeyword ( { keyword : "x-range" , type : "number" , code ( cxt ) { const { gen , schema : [ min , max ] , parentSchema , data , } = cxt ; const eq = parentSchema . exclusiveRange ? nil : _ `=` ; const minOk = gen . const ( "minOk" , _ `${ data } >${ eq } ${ min }` ) ; const maxOk = gen . const ( "maxOk" , _ `${ data } <${ eq } ${ max }` ) ; cxt . setParams ( { minOk , maxOk , eq } ) ; cxt . pass ( _ `${ minOk } && ${ maxOk }` ) ; } , error : { message : ( { params : { minOk , eq } , schema : [ min , max ] } ) => _ `${ minOk } ? "should be <${ eq } ${ max }" : "should be >${ eq } ${ min }"` , params : ( { params : { minOk , eq } , schema : [ min , max ] , parentSchema } ) => _ `{
            comparison: ${ minOk } ? "<${ eq }" : ">${ eq }",
            limit: ${ minOk } ? ${ max } : ${ min },
            exclusive: ${ ! ! parentSchema . exclusiveRange }
          }` , } , } ) ; } ) ; } ) ; describe ( '$data reference support with "validate" keywords (with $data option)' , ( ) => { beforeEach ( ( ) => { instances = getAjvAllInstances ( { allErrors : true , verbose : true , inlineRefs : false , } , { $data : true , allowUnionTypes : true } ) ; ajv = instances [ 0 ] ; } ) ; it ( 'should validate "interpreted" rule' , ( ) => { testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , validate : validateEven , } ) ; function validateEven ( schema , data ) { if ( typeof schema != "boolean" ) return false ; return data % 2 ? ! schema : schema ; } } ) ; it ( 'should validate rule with "compile" and "validate" funcs' , ( ) => { let compileCalled ; testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , compile : compileEven , validate : validateEven , } ) ; compileCalled . should . equal ( true ) ; function validateEven ( schema , data ) { if ( typeof schema != "boolean" ) return false ; return data % 2 ? ! schema : schema ; } function compileEven ( schema ) { compileCalled = true ; if ( typeof schema != "boolean" ) { throw new Error ( 'The value of "even" keyword must be boolean' ) ; } return schema ? isEven : isOdd ; } function isEven ( data ) { return data % 2 === 0 ; } function isOdd ( data ) { return data % 2 !== 0 ; } } ) ; it ( 'should validate with "compile" and "validate" funcs with meta-schema' , ( ) => { let compileCalled ; testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , compile : compileEven , validate : validateEven , metaSchema : { type : "boolean" } , } ) ; compileCalled . should . equal ( true ) ; shouldBeInvalidSchema ( { type : "number" , "x-even-$data" : "false" , } ) ; function validateEven ( schema , data ) { return data % 2 ? ! schema : schema ; } function compileEven ( schema ) { compileCalled = true ; return schema ? isEven : isOdd ; } function isEven ( data ) { return data % 2 === 0 ; } function isOdd ( data ) { return data % 2 !== 0 ; } } ) ; it ( 'should validate rule with "macro" and "validate" funcs' , ( ) => { let macroCalled ; testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , macro : macroEven , validate : validateEven , } , 2 ) ; macroCalled . should . equal ( true ) ; function validateEven ( schema , data ) { if ( typeof schema != "boolean" ) return false ; return data % 2 ? ! schema : schema ; } function macroEven ( schema ) { macroCalled = true ; if ( schema === true ) return { multipleOf : 2 } ; if ( schema === false ) return { not : { multipleOf : 2 } } ; throw new Error ( 'Schema for "even" keyword should be boolean' ) ; } } ) ; it ( 'should validate with "macro" and "validate" funcs with meta-schema' , ( ) => { let macroCalled ; testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , macro : macroEven , validate : validateEven , metaSchema : { type : "boolean" } , } , 2 ) ; macroCalled . should . equal ( true ) ; shouldBeInvalidSchema ( { type : "number" , "x-even-$data" : "false" , } ) ; function validateEven ( schema , data ) { return data % 2 ? ! schema : schema ; } function macroEven ( schema ) { macroCalled = true ; if ( schema === true ) return { multipleOf : 2 } ; if ( schema === false ) return { not : { multipleOf : 2 } } ; } } ) ; it ( 'should validate rule with "code" keyword' , ( ) => { testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , code ( cxt ) { const { gen , schemaCode : s , data } = cxt ; gen . if ( _ `${ s } !== undefined` ) ; cxt . pass ( _ `typeof ${ s } == "boolean" && (${ data } % 2 ? !${ s } : ${ s })` ) ; } , } ) ; } ) ; it ( 'should validate with "code" and meta-schema' , ( ) => { testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , $data : true , code ( cxt ) { const { gen , schemaCode : s , data } = cxt ; gen . if ( _ `${ s } !== undefined` ) ; cxt . pass ( _ `typeof ${ s } == "boolean" && (${ data } % 2 ? !${ s } : ${ s })` ) ; } , metaSchema : { type : "boolean" } , } ) ; shouldBeInvalidSchema ( { type : "number" , "x-even-$data" : "false" , } ) ; } ) ; it ( 'should fail if "macro" keyword definition has "$data" but no "code" or "validate"' , ( ) => { should . throw ( ( ) => { ajv . addKeyword ( { keyword : "even" , type : "number" , $data : true , macro : ( ) => { return { } ; } , } ) ; } , /\$data keyword must have "code" or "validate" function/ ) ; } ) ; it ( "should support schemaType with $data" , ( ) => { testEvenKeyword$data ( { keyword : "x-even-$data" , type : "number" , schemaType : "boolean" , $data : true , validate : validateEven , } ) ; function validateEven ( schema , data ) { return data % 2 ? ! schema : schema ; } } ) ; } ) ; function testEvenKeyword ( evenDefinition , numErrors = 1 ) { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( evenDefinition ) ; const schema = { type : [ "number" , "string" ] , "x-even" : true , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , 2 ) ; shouldBeValid ( validate , "abc" ) ; shouldBeInvalid ( validate , 2.5 , numErrors ) ; shouldBeInvalid ( validate , 3 , numErrors ) ; } ) ; } function testEvenKeyword$data ( definition , numErrors = 1 ) { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( definition ) ; let schema = { type : [ "number" , "string" ] , "x-even-$data" : true , } ; let validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , 2 ) ; shouldBeValid ( validate , "abc" ) ; shouldBeInvalid ( validate , 2.5 , numErrors ) ; shouldBeInvalid ( validate , 3 , numErrors ) ; schema = { type : "object" , properties : { data : { type : [ "number" , "string" ] , "x-even-$data" : { $data : "1/evenValue" } , } , evenValue : { } , } , } ; validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , { data : 2 , evenValue : true } ) ; shouldBeInvalid ( validate , { data : 2 , evenValue : false } ) ; shouldBeValid ( validate , { data : "abc" , evenValue : true } ) ; shouldBeValid ( validate , { data : "abc" , evenValue : false } ) ; shouldBeInvalid ( validate , { data : 2.5 , evenValue : true } ) ; shouldBeValid ( validate , { data : 2.5 , evenValue : false } ) ; shouldBeInvalid ( validate , { data : 3 , evenValue : true } ) ; shouldBeValid ( validate , { data : 3 , evenValue : false } ) ; shouldBeInvalid ( validate , { data : 2 , evenValue : "true" } ) ; shouldBeValid ( validate , { data : 2 } ) ; shouldBeValid ( validate , { data : 3 } ) ; } ) ; } function testConstantKeyword ( definition , numErrors ? ) { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( definition ) ; const schema = { myConstant : "abc" } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , "abc" ) ; shouldBeInvalid ( validate , 2 , numErrors ) ; shouldBeInvalid ( validate , { } , numErrors ) ; } ) ; } function testMultipleConstantKeyword ( definition , numErrors ? ) { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( definition ) ; const schema = { type : [ "object" , "array" ] , properties : { a : { "x-constant" : 1 } , b : { "x-constant" : 1 } , } , additionalProperties : { "x-constant" : { foo : "bar" } } , items : { "x-constant" : { foo : "bar" } } , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , { a : 1 , b : 1 } ) ; shouldBeInvalid ( validate , { a : 2 , b : 1 } , numErrors ) ; shouldBeValid ( validate , { a : 1 , c : { foo : "bar" } } ) ; shouldBeInvalid ( validate , { a : 1 , c : { foo : "baz" } } , numErrors ) ; shouldBeValid ( validate , [ { foo : "bar" } ] ) ; shouldBeValid ( validate , [ { foo : "bar" } , { foo : "bar" } ] ) ; shouldBeInvalid ( validate , [ 1 ] , numErrors ) ; } ) ; } function testRangeKeyword ( definition , createsErrors ? , numErrors ? ) { instances . forEach ( ( _ajv ) => { _ajv . addKeyword ( definition ) ; _ajv . addKeyword ( { keyword : "exclusiveRange" , schemaType : "boolean" } ) ; let schema = { type : [ "number" , "string" ] , "x-range" : [ 2 , 4 ] , } ; let validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , 2 ) ; shouldBeValid ( validate , 3 ) ; shouldBeValid ( validate , 4 ) ; shouldBeValid ( validate , "abc" ) ; shouldBeInvalid ( validate , 1.99 , numErrors ) ; if ( createsErrors ) { shouldBeRangeError ( validate . errors ?. [ 0 ] , "" , "#/x-range" , ">=" , 2 ) ; } shouldBeInvalid ( validate , 4.01 , numErrors ) ; if ( createsErrors ) { shouldBeRangeError ( validate . errors ?. [ 0 ] , "" , "#/x-range" , "<=" , 4 ) ; } schema = { type : "object" , properties : { foo : { type : [ "number" ] , "x-range" : [ 2 , 4 ] , exclusiveRange : true , } , } , } ; validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , { foo : 2.01 } ) ; shouldBeValid ( validate , { foo : 3 } ) ; shouldBeValid ( validate , { foo : 3.99 } ) ; shouldBeInvalid ( validate , { foo : 2 } , numErrors ) ; if ( createsErrors ) { shouldBeRangeError ( validate . errors ?. [ 0 ] , "/foo" , "#/properties/foo/x-range" , ">" , 2 , true ) ; } shouldBeInvalid ( validate , { foo : 4 } , numErrors ) ; if ( createsErrors ) { shouldBeRangeError ( validate . errors ?. [ 0 ] , "/foo" , "#/properties/foo/x-range" , "<" , 4 , true ) ; } } ) ; } function testMultipleRangeKeyword ( definition , numErrors ? ) { instances . forEach ( ( _ajv ) => { _ajv . opts . strictTypes = false ; _ajv . addKeyword ( definition ) ; _ajv . addKeyword ( { keyword : "exclusiveRange" , schemaType : "boolean" } ) ; const schema = { properties : { a : { type : "number" , "x-range" : [ 2 , 4 ] , exclusiveRange : true } , b : { type : "number" , "x-range" : [ 2 , 4 ] , exclusiveRange : false } , } , additionalProperties : { type : "number" , "x-range" : [ 5 , 7 ] } , items : { type : "number" , "x-range" : [ 5 , 7 ] } , } ; const validate = _ajv . compile ( schema ) ; shouldBeValid ( validate , { a : 3.99 , b : 4 } ) ; shouldBeInvalid ( validate , { a : 4 , b : 4 } , numErrors ) ; shouldBeValid ( validate , { a : 2.01 , c : 7 } ) ; shouldBeInvalid ( validate , { a : 2.01 , c : 7.01 } , numErrors ) ; shouldBeValid ( validate , [ 5 , 6 , 7 ] ) ; shouldBeInvalid ( validate , [ 7.01 ] , numErrors ) ; } ) ; } function shouldBeRangeError ( error , instancePath , schemaPath , comparison , limit , exclusive ? ) { delete error . schema ; delete error . data ; error . should . eql ( { keyword : "x-range" , instancePath , schemaPath , message : "should be " + comparison + " " + limit , params : { comparison : comparison , limit : limit , exclusive : ! ! exclusive , } , } ) ; } function validateRangeSchema ( schema , parentSchema ) { const schemaValid = Array . isArray ( schema ) && schema . length === 2 && typeof schema [ 0 ] == "number" && typeof schema [ 1 ] == "number" ; if ( ! schemaValid ) { throw new Error ( "Invalid schema for range keyword, should be array of 2 numbers" ) ; } const exclusiveRangeSchemaValid = parentSchema . exclusiveRange === undefined || typeof parentSchema . exclusiveRange == "boolean" ; if ( ! exclusiveRangeSchemaValid ) { throw new Error ( "Invalid schema for exclusiveRange keyword, should be boolean" ) ; } } function shouldBeValid ( validate , data ) { validate ( data ) . should . equal ( true ) ; should . not . exist ( validate . errors ) ; } function shouldBeInvalid ( validate , data , numErrors = 1 ) { validate ( data ) . should . equal ( false ) ; validate . errors . should . have . length ( numErrors ) ; } function shouldBeInvalidSchema ( schema , msg = /keyword .+ value is invalid at path/ ) { instances . forEach ( ( _ajv ) => { should . throw ( ( ) => { _ajv . compile ( schema ) ; } , msg ) ; } ) ; } describe ( "addKeyword method" , ( ) => { const TEST_TYPES = [ undefined , "number" , "string" , "boolean" , [ "number" , "string" ] ] ; it ( "should throw if defined keyword is passed" , ( ) => { testThrow ( [ "minimum" , "maximum" , "multipleOf" , "minLength" , "maxLength" ] ) ; testThrowDuplicate ( "user-defined" ) ; function testThrow ( keywords ) { TEST_TYPES . forEach ( ( dataType , index ) => { should . throw ( ( ) => { _addKeyword ( keywords [ index ] , dataType ) ; } , /already defined/ ) ; } ) ; } function testThrowDuplicate ( keywordPrefix ) { let index = 0 ; TEST_TYPES . forEach ( ( dataType1 ) => { TEST_TYPES . forEach ( ( dataType2 ) => { const keyword = keywordPrefix + index ++ ; _addKeyword ( keyword , dataType1 ) ; should . throw ( ( ) => { _addKeyword ( keyword , dataType2 ) ; } , /already defined/ ) ; } ) ; } ) ; } } ) ; it ( "should throw if keyword is not a valid name" , ( ) => { should . not . throw ( ( ) => { ajv . addKeyword ( "mykeyword" ) ; } ) ; should . not . throw ( ( ) => { ajv . addKeyword ( "hyphens-are-valid" ) ; } ) ; should . not . throw ( ( ) => { ajv . addKeyword ( "colons:are-valid" ) ; } ) ; should . throw ( ( ) => { ajv . addKeyword ( "single-'quote-not-valid" ) ; } , /invalid name/ ) ; should . throw ( ( ) => { ajv . addKeyword ( "3-start-with-number-not-valid" ) ; } , /invalid name/ ) ; should . throw ( ( ) => { ajv . addKeyword ( "-start-with-hyphen-not-valid" ) ; } , /invalid name/ ) ; should . throw ( ( ) => { ajv . addKeyword ( "spaces not valid" ) ; } , /invalid name/ ) ; } ) ; it ( "should return instance of itself" , ( ) => { const res = ajv . addKeyword ( "any" ) ; res . should . equal ( ajv ) ; } ) ; it ( "should throw if unknown type is passed" , ( ) => { should . throw ( ( ) => { _addKeyword ( "user-defined1" , "wrongtype" ) ; } , /type must be JSONType/ ) ; should . throw ( ( ) => { _addKeyword ( "user-defined2" , [ "number" , "wrongtype" ] ) ; } , /type must be JSONType/ ) ; should . throw ( ( ) => { _addKeyword ( "user-defined3" , [ "number" , undefined ] ) ; } , /type must be JSONType/ ) ; } ) ; it ( "should support old API addKeyword" , ( ) => { ajv = new _Ajv ( { logger : false } ) ; ajv . addKeyword ( "min" , { type : "number" , schemaType : "number" , validate : ( schema , data ) => data >= schema , } ) ; const validate = ajv . compile ( { type : "number" , min : 0 , } ) ; validate ( 1 ) . should . equal ( true ) ; validate ( - 1 ) . should . equal ( false ) ; } ) ; function _addKeyword ( keyword , dataType ) { ajv . addKeyword ( { keyword , type : dataType , validate : ( ) => true , } ) ; } } ) ; describe ( "getKeyword" , ( ) => { it ( "should return false for unknown keywords" , ( ) => { ajv . getKeyword ( "unknown" ) . should . equal ( false ) ; } ) ; it ( "should return keyword definition" , ( ) => { const definition = { keyword : "mykeyword" , validate : ( ) => true , } ; ajv . addKeyword ( definition ) ; const def = ajv . getKeyword ( "mykeyword" ) ; assert ( typeof def == "object" ) ; def . keyword . should . equal ( "mykeyword" ) ; } ) ; } ) ; describe ( "removeKeyword" , ( ) => { it ( "should remove and allow redefining keyword" , ( ) => { ajv = new _Ajv ( { strict : false } ) ; ajv . addKeyword ( { keyword : "positive" , type : "number" , validate : ( _schema , data ) => data > 0 , } ) ; const schema = { type : "number" , positive : true } ; let validate = ajv . compile ( schema ) ; validate ( 0 ) . should . equal ( false ) ; validate ( 1 ) . should . equal ( true ) ; should . throw ( ( ) => { ajv . addKeyword ( { keyword : "positive" , type : "number" , validate : function ( _sch , data ) { return data >= 0 ; } , } ) ; } , /already defined/ ) ; ajv . removeKeyword ( "positive" ) ; ajv . removeSchema ( schema ) ; validate = ajv . compile ( schema ) ; validate ( - 1 ) . should . equal ( true ) ; ajv . removeSchema ( schema ) ; ajv . addKeyword ( { keyword : "positive" , type : "number" , validate : function ( _sch , data ) { return data >= 0 ; } , } ) ; validate = ajv . compile ( schema ) ; validate ( - 1 ) . should . equal ( false ) ; validate ( 0 ) . should . equal ( true ) ; validate ( 1 ) . should . equal ( true ) ; } ) ; it ( "should remove and allow redefining standard keyword" , ( ) => { ajv = new _Ajv ( { strict : false } ) ; const schema = { minimum : 1 } ; let validate = ajv . compile ( schema ) ; validate ( 0 ) . should . equal ( false ) ; validate ( 1 ) . should . equal ( true ) ; validate ( 2 ) . should . equal ( true ) ; ajv . removeKeyword ( "minimum" ) ; ajv . removeSchema ( schema ) ; validate = ajv . compile ( schema ) ; validate ( 0 ) . should . equal ( true ) ; validate ( 1 ) . should . equal ( true ) ; validate ( 2 ) . should . equal ( true ) ; ajv . addKeyword ( { keyword : "minimum" , type : "number" , validate : ( sch , data ) => data > sch , } ) ; ajv . removeSchema ( schema ) ; validate = ajv . compile ( schema ) ; validate ( 0 ) . should . equal ( false ) ; validate ( 1 ) . should . equal ( false ) ; validate ( 2 ) . should . equal ( true ) ; } ) ; it ( "should return instance of itself" , ( ) => { const res = ajv . addKeyword ( "any" ) . removeKeyword ( "any" ) ; res . should . equal ( ajv ) ; } ) ; } ) ; describe ( "user-defined keywords mutating data" , ( ) => { it ( "should NOT update data without option modifying" , ( ) => { should . throw ( ( ) => { testModifying ( false ) ; } , /expected false to equal true/ ) ; } ) ; it ( "should update data with option modifying" , ( ) => { testModifying ( true ) ; } ) ; function testModifying ( withOption ) { const collectionFormat = { csv : function ( data , { parentData , parentDataProperty } ) { parentData [ parentDataProperty ] = data . split ( "," ) ; return true ; } , } ; ajv . addKeyword ( { keyword : "collectionFormat" , type : "string" , modifying : withOption , compile : function ( schema ) { return collectionFormat [ schema ] ; } , metaSchema : { enum : [ "csv" ] , } , } ) ; const validate = ajv . compile ( { type : "object" , properties : { foo : { allOf : [ { type : "string" , collectionFormat : "csv" , } , { type : "array" , items : { type : "string" } , } , ] , } , } , additionalProperties : false , } ) ; const obj = { foo : "bar,baz,quux" } ; validate ( obj ) . should . equal ( true ) ; obj . should . eql ( { foo : [ "bar" , "baz" , "quux" ] } ) ; } } ) ; describe ( '"validate" keywords with predefined validation result' , ( ) => { it ( "should ignore result from validation function" , ( ) => { ajv . addKeyword ( { keyword : "pass" , validate : ( ) => false , valid : true , } ) ; ajv . addKeyword ( { keyword : "fail" , validate : ( ) => true , valid : false , } ) ; ajv . validate ( { pass : "" } , 1 ) . should . equal ( true ) ; ajv . validate ( { fail : "" } , 1 ) . should . equal ( false ) ; } ) ; } ) ; describe ( '"dependencies" in keyword definition' , ( ) => { it ( "should require properties in the parent schema" , ( ) => { ajv . addKeyword ( { keyword : "allRequired" , type : "object" , macro : ( schema , parentSchema ) => schema ? { required : Object . keys ( parentSchema . properties ) } : true , schemaType : "boolean" , dependencies : [ "properties" ] , } ) ; const invalidSchema = { type : "object" , allRequired : true , } ; should . throw ( ( ) => { ajv . compile ( invalidSchema ) ; } , /parent schema must have dependencies of allRequired: properties/ ) ; const schema = { type : "object" , properties : { foo : true , } , allRequired : true , } ; const v = ajv . compile ( schema ) ; v ( { foo : 1 } ) . should . equal ( true ) ; v ( { } ) . should . equal ( false ) ; } ) ; } ) ; } ) ;