Skip to content

Commit f9ad09b

Browse files
feat: Implemented schema-validator and tests
1 parent 5ab0a13 commit f9ad09b

10 files changed

+344
-0
lines changed

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "json-validations-lib",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"test-units": "cross-env BUILD_ENV=test DEBUG=amio* mocha test/test_unit/**/*.test.js --reporter mocha-multi-reporters --reporter-options configFile=test/mocha-reporters-config.json"
7+
},
8+
"dependencies": {
9+
"ajv": "6.10.2",
10+
"logzio-node-debug": "^2.0.0",
11+
"ramda": "0.26.1",
12+
"valid-url": "1.0.9"
13+
},
14+
"devDependencies": {
15+
"chai": "4.2.0",
16+
"chai-as-promised": "7.1.1",
17+
"mocha": "6.1.4",
18+
"mocha-junit-reporter": "1.22.0",
19+
"mocha-multi-reporters": "1.1.7",
20+
"mochawesome": "3.1.2"
21+
}
22+
}

src/async-validator.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class AsyncValidator {
2+
3+
async validate(data) {
4+
throw Error('validate() must be implemented in children')
5+
}
6+
7+
/** Follows AJV error to be compatible with Schema errors */
8+
createError(path = [], keyword, data, params) {
9+
return {
10+
dataPath: path.join('.'),
11+
keyword,
12+
data,
13+
params
14+
}
15+
}
16+
17+
}
18+
19+
module.exports = AsyncValidator
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class SchemaValidatorError extends Error{
2+
constructor(message, field = undefined, rejectedValue = undefined) {
3+
super()
4+
this.field = field
5+
this.rejected_value = rejectedValue
6+
this.message = message
7+
}
8+
9+
toObject(){
10+
return {
11+
message: this.message,
12+
field: this.field,
13+
rejected_value: this.rejected_value
14+
}
15+
}
16+
}
17+
18+
module.exports = SchemaValidatorError

src/schema-validator.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const Ajv = require('ajv')
2+
const errorConverter = require('./utils/validation-errors-converter')
3+
const urlValidator = require('valid-url')
4+
const debug = require('logzio-node-debug').debug('json-validations-lib:' + require('path').basename(__filename))
5+
6+
class SchemaValidator {
7+
8+
constructor(schemaId, schemas) {
9+
this.schemaValidator = new Ajv({verbose: true, schemas: schemas}) // verbose to enable errors.data field
10+
this.schemaValidator.addFormat('httpUrl', urlValidator.isWebUri)
11+
this._validate = this.schemaValidator.getSchema(schemaId)
12+
}
13+
14+
/** @returns null OR error*/
15+
validate(data) {
16+
const valid = this._validate(data)
17+
if(valid) return null
18+
19+
const validationErrors = this._validate.errors
20+
debug('schema validation failed', this._validate.errors)
21+
throw errorConverter.convertValidationError(validationErrors)
22+
}
23+
}
24+
25+
module.exports = SchemaValidator
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const R = require('ramda')
2+
const SchemaValidatorError = require('../errors/schema-validator-error')
3+
4+
function convertValidationError(error) {
5+
const errorObject = Array.isArray(error) ? error[0] : error
6+
7+
const propName = errorObject.dataPath.split('.').pop()
8+
const params = errorObject.params
9+
10+
switch(errorObject.keyword) {
11+
case 'type':
12+
return new SchemaValidatorError(`Property '${propName}' must be ${params.type}.`, errorObject.dataPath, errorObject.data)
13+
case 'additionalProperties':
14+
return new SchemaValidatorError(`Property '${params.additionalProperty}' is not supported.`,
15+
errorObject.dataPath + '.' + params.additionalProperty)
16+
case 'required': {
17+
const fieldPath = (errorObject.dataPath) ? errorObject.dataPath : '.'
18+
return new SchemaValidatorError(`Missing property '${params.missingProperty}'.`, fieldPath)
19+
}
20+
case 'const':
21+
return new SchemaValidatorError(`Property '${propName}' must have value '${params.allowedValue}'.`, errorObject.dataPath, errorObject.data)
22+
case 'enum':
23+
return convertEnum(errorObject.data, errorObject.dataPath, params, propName, errorObject.schemaPath)
24+
case 'format':
25+
if(errorObject.params.format === 'httpUrl') {
26+
const message = `Property '${propName}' must be a valid URL. Current value is "${errorObject.data}"`
27+
return new SchemaValidatorError(message, errorObject.dataPath, errorObject.data)
28+
}
29+
// Intentional fall through
30+
default:
31+
return new SchemaValidatorError(`Property '${propName}' ${errorObject.message}`, errorObject.dataPath, errorObject.data)
32+
}
33+
}
34+
35+
function convertEnum(data, dataPath, params, propName, schemaPath) {
36+
const schemaParent = R.pipe( // #/propertyNames/enum
37+
R.split('/'), // ['#', 'propertyNames', 'enum']
38+
R.dropLast(1), // ['#', 'propertyNames']
39+
R.last // 'propertyNames'
40+
)(schemaPath)
41+
42+
if(schemaParent === 'propertyNames') {
43+
const path = dataPath || '.'
44+
const message = `Property '${data}' at '${path}' does not match any allowed property: ${params.allowedValues.join(', ')}.`
45+
const fieldPath = `${dataPath}.${data}`
46+
return new SchemaValidatorError(message, fieldPath)
47+
}
48+
49+
const message = `Property '${propName}' with value '${data}' does not match any allowed value: ${params.allowedValues.join(', ')}.`
50+
return new SchemaValidatorError(message, dataPath, data)
51+
52+
}
53+
54+
module.exports = {
55+
convertValidationError
56+
}

test/global-test-hooks.js

Whitespace-only changes.

test/mocha-reporters-config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"reporterEnabled": "mocha-junit-reporter, mochawesome",
3+
"mochaJunitReporterReporterOptions": {
4+
"mochaFile": "test-results.xml"
5+
}
6+
}

test/mocha.opts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test/global-test-hooks.js
2+
--timeout 20000
3+
--exit
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const expect = require('chai').expect
2+
const SchemaValidator = require('../../src/schema-validator')
3+
4+
describe('Schema Validator', () => {
5+
6+
const schema = {
7+
$id: 'http://example.com/schemas/schema.json',
8+
type: 'object',
9+
required: [
10+
'a'
11+
]
12+
}
13+
const schemaValidator = new SchemaValidator(schema.$id, [schema])
14+
15+
it('Validation passed', () => {
16+
const error = schemaValidator.validate({a: 1})
17+
expect(error).to.be.null
18+
})
19+
20+
it('Validation failed', () => {
21+
const error = schemaValidator.validate({})
22+
expect(error).to.eql({
23+
field: '.',
24+
message: "Missing property '.a'."
25+
})
26+
})
27+
})
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
const expect = require('chai').expect
2+
const SchemaValidator = require('../../src/schema-validator')
3+
4+
describe('validation-error-converter test', () => {
5+
6+
const schema = {
7+
$id: 'http://example.com/schemas/schema.json',
8+
type: 'object',
9+
additionalProperties: false,
10+
properties: {
11+
typeError: {
12+
type: 'string'
13+
},
14+
additionalPropertyError: {
15+
type: 'object',
16+
additionalProperties: false,
17+
properties: {
18+
a: {
19+
type: 'string'
20+
}
21+
}
22+
},
23+
requiredError: {
24+
type: 'object',
25+
required: ['a'],
26+
properties: {
27+
a: {
28+
type: 'string'
29+
}
30+
}
31+
},
32+
constError: {
33+
type: 'string',
34+
const: 'dummy'
35+
},
36+
enumError: {
37+
type: 'string',
38+
enum: [
39+
'1',
40+
'2',
41+
'3'
42+
]
43+
},
44+
propEnumError: {
45+
type: 'object',
46+
propertyNames: {
47+
enum: [
48+
'a',
49+
'b',
50+
'c'
51+
]
52+
}
53+
},
54+
formatUrlError: {
55+
type: 'string',
56+
format: 'httpUrl'
57+
},
58+
formatError: {
59+
type: 'string',
60+
format: 'email'
61+
}
62+
}
63+
}
64+
const schemaValidator = new SchemaValidator(schema.$id, [schema])
65+
66+
it('type', () => {
67+
const data = {
68+
typeError: {}
69+
}
70+
71+
const result = schemaValidator.validate(data)
72+
expect(result).to.include({
73+
field: '.typeError',
74+
message: "Property 'typeError' must be string.",
75+
rejected_value: data.typeError
76+
})
77+
})
78+
79+
it('additionalProperties', () => {
80+
const data = {
81+
additionalPropertyError: {
82+
d: {}
83+
}
84+
}
85+
86+
const result = schemaValidator.validate(data)
87+
expect(result).to.include({
88+
field: '.additionalPropertyError.d',
89+
message: "Property 'd' is not supported."
90+
})
91+
})
92+
93+
it('required', () => {
94+
const data = {
95+
requiredError: {}
96+
}
97+
98+
const result = schemaValidator.validate(data)
99+
expect(result).to.include({
100+
field: '.requiredError',
101+
message: "Missing property 'a'."
102+
})
103+
})
104+
105+
it('const', () => {
106+
const data = {
107+
constError: 'a'
108+
}
109+
110+
const result = schemaValidator.validate(data)
111+
expect(result).to.include({
112+
field: '.constError',
113+
message: "Property 'constError' must have value 'dummy'.",
114+
rejected_value: 'a'
115+
})
116+
})
117+
118+
it('enum - values', () => {
119+
const data = {
120+
enumError: 'a'
121+
}
122+
123+
const result = schemaValidator.validate(data)
124+
expect(result).to.include({
125+
field: '.enumError',
126+
message: "Property 'enumError' with value 'a' does not match any allowed value: 1, 2, 3.",
127+
rejected_value: 'a'
128+
})
129+
})
130+
131+
it('enum - properties', () => {
132+
const data = {
133+
propEnumError: {d: {}}
134+
}
135+
136+
const result = schemaValidator.validate(data)
137+
expect(result).to.include({
138+
field: '.propEnumError.d',
139+
message: "Property 'd' at '.propEnumError' does not match any allowed property: a, b, c.",
140+
})
141+
})
142+
143+
it('format URL', () => {
144+
const data = {
145+
formatUrlError: 'dummy'
146+
}
147+
148+
const result = schemaValidator.validate(data)
149+
expect(result).to.include({
150+
field: '.formatUrlError',
151+
message: `Property 'formatUrlError' must be a valid URL. Current value is "dummy"`,
152+
rejected_value: 'dummy'
153+
})
154+
})
155+
156+
it('format other', () => {
157+
const data = {
158+
formatError: 'dummy'
159+
}
160+
161+
const result = schemaValidator.validate(data)
162+
expect(result).to.include({
163+
field: '.formatError',
164+
message: `Property 'formatError' should match format "email"`,
165+
rejected_value: 'dummy'
166+
})
167+
})
168+
})

0 commit comments

Comments
 (0)