arstotzka 0.13.0 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +38 -29
- package/package.json +2 -2
- package/tests.js +115 -0
- package/usage.js +53 -83
package/index.js
CHANGED
|
@@ -8,7 +8,8 @@ export const ERRORS = {
|
|
|
8
8
|
targetIsNull: "Passed object or array item is null",
|
|
9
9
|
functionExpected: "Expected function as dynamic constraint",
|
|
10
10
|
objectExpected: "Expected object",
|
|
11
|
-
anyFailed: "None of ANY_OF constraints are met"
|
|
11
|
+
anyFailed: "None of ANY_OF constraints are met",
|
|
12
|
+
parsingError: "Schema parsing error"
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export const OPTIONAL = Symbol();
|
|
@@ -35,6 +36,7 @@ export function DYNAMIC(constraints){
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const TYPE = t => x => typeof x == t;
|
|
39
|
+
const IS_ARRAY = x => Array.isArray(x);
|
|
38
40
|
|
|
39
41
|
const CONSTRAINT = Symbol();
|
|
40
42
|
const FC_ARRAY = Symbol(); // https://www.youtube.com/watch?v=qSqXGeJJBaI
|
|
@@ -74,34 +76,38 @@ function constraint(f, failMessageId, expected){
|
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function parseSchema(schemaProperty){
|
|
79
|
+
function parseSchema(schemaProperty, errors){
|
|
78
80
|
function unify(raw){
|
|
79
|
-
if (raw
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
if (raw !== null && raw !== undefined){
|
|
82
|
+
if (raw.type == CONSTRAINT) return raw;
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(raw)){
|
|
85
|
+
return raw.map(c => unify(c)).filter(raw => raw !== null);
|
|
86
|
+
} else {
|
|
87
|
+
switch (typeof raw){
|
|
88
|
+
case ("string"): {
|
|
89
|
+
if (raw == "array")
|
|
90
|
+
return constraint(IS_ARRAY, "typeMismatch", raw);
|
|
91
|
+
else
|
|
92
|
+
return constraint(TYPE(raw), "typeMismatch", raw);
|
|
93
|
+
}
|
|
94
|
+
case ("function"): {
|
|
95
|
+
return constraint(raw, "customFail", undefined);
|
|
96
|
+
}
|
|
97
|
+
case ("object"): {
|
|
98
|
+
return constraint(FC_NESTED, null, raw);
|
|
99
|
+
}
|
|
100
|
+
case ("symbol"): {
|
|
101
|
+
return raw;
|
|
102
|
+
}
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
106
|
+
errors.push(error("<schema>", "parsingError", undefined, raw));
|
|
107
|
+
return null;
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
let intermediate = unify(schemaProperty);
|
|
110
|
+
let intermediate = unify(schemaProperty) || [];
|
|
105
111
|
if (!Array.isArray(intermediate))
|
|
106
112
|
intermediate = [intermediate];
|
|
107
113
|
return [
|
|
@@ -134,7 +140,7 @@ function checkValue(propertyName, value, constraints, options){
|
|
|
134
140
|
const schemaKeys = Object.keys(constraint.expected);
|
|
135
141
|
|
|
136
142
|
for (let key of schemaKeys){
|
|
137
|
-
const [subConstraints, flags] = parseSchema(constraint.expected[key]);
|
|
143
|
+
const [subConstraints, flags] = parseSchema(constraint.expected[key], errors);
|
|
138
144
|
|
|
139
145
|
if (!targetKeys.includes(key)){
|
|
140
146
|
if (!flags.includes(OPTIONAL)){
|
|
@@ -162,7 +168,7 @@ function checkValue(propertyName, value, constraints, options){
|
|
|
162
168
|
break;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
const [subConstraints, flags] = parseSchema(constraint.expected);
|
|
171
|
+
const [subConstraints, flags] = parseSchema(constraint.expected, errors);
|
|
166
172
|
|
|
167
173
|
let indexCounter = 0;
|
|
168
174
|
for (let item of value){
|
|
@@ -181,8 +187,10 @@ function checkValue(propertyName, value, constraints, options){
|
|
|
181
187
|
let counter = 0;
|
|
182
188
|
|
|
183
189
|
for (let subSchema of subSchemas){
|
|
184
|
-
const
|
|
190
|
+
const subParseErrors = [];
|
|
191
|
+
const [subConstraints, flags] = parseSchema(subSchema, subParseErrors);
|
|
185
192
|
const caseErrors = checkValue(`${propertyName}.<any#${counter}>`, value, subConstraints, options);
|
|
193
|
+
caseErrors.push(...subParseErrors);
|
|
186
194
|
++counter;
|
|
187
195
|
subErrors.push(caseErrors);
|
|
188
196
|
if (caseErrors.length == 0) {
|
|
@@ -203,7 +211,7 @@ function checkValue(propertyName, value, constraints, options){
|
|
|
203
211
|
break;
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
const [subConstraints, flags] = parseSchema(constraintCallback(value))
|
|
214
|
+
const [subConstraints, flags] = parseSchema(constraintCallback(value), errors)
|
|
207
215
|
const subErrors = checkValue(propertyName, value, subConstraints, options);
|
|
208
216
|
errors.push(...subErrors);
|
|
209
217
|
|
|
@@ -226,14 +234,15 @@ function checkValue(propertyName, value, constraints, options){
|
|
|
226
234
|
export function validate(target, schema = {}, options = {}){
|
|
227
235
|
options = Object.assign(VALIDATION_DEFAULTS, options)
|
|
228
236
|
|
|
229
|
-
const
|
|
237
|
+
const parseErrors = [];
|
|
238
|
+
const [constraints, flags] = parseSchema(schema, parseErrors);
|
|
230
239
|
|
|
231
240
|
if (flags.length > 0) {
|
|
232
241
|
console.error("Flags can't be used at schema's root")
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
const errors = checkValue("", target, constraints, options);
|
|
236
|
-
|
|
245
|
+
errors.push(...parseErrors)
|
|
237
246
|
errors.forEach(e => {
|
|
238
247
|
if (e.propertyName?.startsWith("."))
|
|
239
248
|
e.propertyName = e.propertyName.slice(1);
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arstotzka",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "JS validation utility",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "
|
|
8
|
+
"test": "node tests.js"
|
|
9
9
|
},
|
|
10
10
|
"keywords": [
|
|
11
11
|
"JSON",
|
package/tests.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as Arstotzka from "./index.js";
|
|
2
|
+
|
|
3
|
+
const schema = {
|
|
4
|
+
title: "string",
|
|
5
|
+
array: [Arstotzka.ARRAY_OF("number"), x => x.length > 1],
|
|
6
|
+
arrayOfObjs: Arstotzka.ARRAY_OF({
|
|
7
|
+
id: "number",
|
|
8
|
+
name: "string"
|
|
9
|
+
}),
|
|
10
|
+
notArray: Arstotzka.ARRAY_OF("string"),
|
|
11
|
+
arrayOof: Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number")),
|
|
12
|
+
positiveArray: Arstotzka.ARRAY_OF(x => x > 0),
|
|
13
|
+
nested: {
|
|
14
|
+
parseableNumber: [x => !isNaN(parseInt(x, 10))],
|
|
15
|
+
anotherNest: [{
|
|
16
|
+
phrase: "string",
|
|
17
|
+
wordCount: "number"
|
|
18
|
+
}, x => x.phrase.split(" ").length == x.wordCount],
|
|
19
|
+
missing: []
|
|
20
|
+
},
|
|
21
|
+
invalidValidator: [x => null.invalid],
|
|
22
|
+
optional: ["number", Arstotzka.OPTIONAL]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const testSubject0 = {
|
|
26
|
+
title: "1337",
|
|
27
|
+
array: [1, 2, 3],
|
|
28
|
+
arrayOfObjs: [
|
|
29
|
+
{id: 0, name: "_"},
|
|
30
|
+
{id: 1, name: "second"},
|
|
31
|
+
{id: 2, name: "3"},
|
|
32
|
+
],
|
|
33
|
+
notArray: [],
|
|
34
|
+
arrayOof: [
|
|
35
|
+
[1, 2, 3],
|
|
36
|
+
[4, 5, 6],
|
|
37
|
+
[1, 2]
|
|
38
|
+
],
|
|
39
|
+
positiveArray: [Infinity, 1, 4, 5],
|
|
40
|
+
nested: {
|
|
41
|
+
parseableNumber: "777",
|
|
42
|
+
anotherNest: {
|
|
43
|
+
phrase:"henlo",
|
|
44
|
+
wordCount: 1
|
|
45
|
+
},
|
|
46
|
+
missing: 0
|
|
47
|
+
},
|
|
48
|
+
invalidValidator: "uhm"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const testSubject1 = {
|
|
52
|
+
title: 1337,
|
|
53
|
+
array: [1, null, 3],
|
|
54
|
+
arrayOfObjs: [
|
|
55
|
+
{id: 0, name: "_"},
|
|
56
|
+
{id: 0,},
|
|
57
|
+
{id: "0", name: "_"}
|
|
58
|
+
],
|
|
59
|
+
notArray: "[1, 2, 3]",
|
|
60
|
+
arrayOof: [
|
|
61
|
+
[1, 2, 3],
|
|
62
|
+
[4, "5", 6],
|
|
63
|
+
null,
|
|
64
|
+
[1, 2]
|
|
65
|
+
],
|
|
66
|
+
positiveArray: [0, 1, 4, -5],
|
|
67
|
+
nested: {
|
|
68
|
+
parseableNumber: "seven",
|
|
69
|
+
extraProperty: "hey",
|
|
70
|
+
anotherNest: {
|
|
71
|
+
phrase:"henlo",
|
|
72
|
+
wordCount: 2
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
invalidValidator: "uhm"
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const schema1 = Arstotzka.DYNAMIC(x => dynSchema[x.type]);
|
|
79
|
+
const dynSchema = [
|
|
80
|
+
{
|
|
81
|
+
type: "number",
|
|
82
|
+
zero: "string"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "number",
|
|
86
|
+
one: ["number", x => x === 1]
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
const testSubject2 = {
|
|
90
|
+
type: 0,
|
|
91
|
+
zero: "0"
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const parseableIntSchema = Arstotzka.ANY_OF(
|
|
95
|
+
"number",
|
|
96
|
+
["string", x => !isNaN(parseInt(x))]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// [target, schema, error count]
|
|
100
|
+
const tests = [
|
|
101
|
+
[null, "array", 1], // null values and non-object constraints
|
|
102
|
+
[testSubject0, schema, 1], // Only an error caused by invalid validator
|
|
103
|
+
[testSubject1, schema, 13], // Full of errors
|
|
104
|
+
["miles", "string", 0], // Any value can be validated, not only objects
|
|
105
|
+
[7, "string", 1], // Same but failed
|
|
106
|
+
[testSubject2, schema1, 0], // Dynamic schema
|
|
107
|
+
["7", parseableIntSchema, 0], // Schema with ANY_OF
|
|
108
|
+
[testSubject0, null, 1] // Invalid schema
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const testResults = tests
|
|
112
|
+
.map(t => Arstotzka.validate(t[0], t[1]).length == t[2])
|
|
113
|
+
.map((testPassed, i) => `Test ${i}: ${testPassed ? "PASSED" : "FAILED"}`)
|
|
114
|
+
.join("\n");
|
|
115
|
+
console.log(testResults);
|
package/usage.js
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
import * as Arstotzka from "./index.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
array: [Arstotzka.ARRAY_OF("number"), x => x.length > 1],
|
|
6
|
-
arrayOfObjs: Arstotzka.ARRAY_OF({
|
|
7
|
-
id: "number",
|
|
8
|
-
name: "string"
|
|
9
|
-
}),
|
|
10
|
-
notArray: Arstotzka.ARRAY_OF("string"),
|
|
11
|
-
arrayOof: Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number")),
|
|
12
|
-
positiveArray: Arstotzka.ARRAY_OF(x => x > 0),
|
|
13
|
-
nested: {
|
|
14
|
-
parseableNumber: [x => !isNaN(parseInt(x, 10))],
|
|
15
|
-
anotherNest: [{
|
|
16
|
-
phrase: "string",
|
|
17
|
-
wordCount: "number"
|
|
18
|
-
}, x => x.phrase.split(" ").length == x.wordCount],
|
|
19
|
-
missing: []
|
|
20
|
-
},
|
|
21
|
-
invalidValidator: [x => null.invalid],
|
|
22
|
-
optional: ["number", Arstotzka.OPTIONAL]
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const testSubject0 = {
|
|
3
|
+
// This value will be validated against schema described below
|
|
4
|
+
const value = {
|
|
26
5
|
title: "1337",
|
|
27
6
|
array: [1, 2, 3],
|
|
28
7
|
arrayOfObjs: [
|
|
@@ -30,81 +9,72 @@ const testSubject0 = {
|
|
|
30
9
|
{id: 1, name: "second"},
|
|
31
10
|
{id: 2, name: "3"},
|
|
32
11
|
],
|
|
33
|
-
|
|
34
|
-
arrayOof: [
|
|
12
|
+
matrix: [
|
|
35
13
|
[1, 2, 3],
|
|
36
14
|
[4, 5, 6],
|
|
37
|
-
[1, 2]
|
|
15
|
+
[1, 2, 0]
|
|
38
16
|
],
|
|
39
17
|
positiveArray: [Infinity, 1, 4, 5],
|
|
40
18
|
nested: {
|
|
41
19
|
parseableNumber: "777",
|
|
42
20
|
anotherNest: {
|
|
43
|
-
phrase:"henlo",
|
|
44
|
-
wordCount:
|
|
21
|
+
phrase:"henlo there",
|
|
22
|
+
wordCount: 2
|
|
45
23
|
},
|
|
46
|
-
|
|
24
|
+
justBeThere: 0
|
|
47
25
|
},
|
|
48
|
-
|
|
26
|
+
booleanOrNumber: 0,
|
|
27
|
+
dynamic: 69
|
|
49
28
|
};
|
|
50
29
|
|
|
51
|
-
const
|
|
52
|
-
title
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
30
|
+
const schema = { // Require value to be an object
|
|
31
|
+
// Require typeof value.title to be "string"
|
|
32
|
+
title: "string",
|
|
33
|
+
|
|
34
|
+
//Require value.array to be an array of numbers AND to be longer than 1 element
|
|
35
|
+
array: [Arstotzka.ARRAY_OF("number"), x => x.length > 1],
|
|
36
|
+
|
|
37
|
+
// Require each element of value.arrayOfObjs to be valid according to provided schema
|
|
38
|
+
arrayOfObjs: Arstotzka.ARRAY_OF({
|
|
39
|
+
id: "number",
|
|
40
|
+
name: "string"
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// Require each element of value.matrix to be an array of numbers
|
|
44
|
+
matrix: Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number")),
|
|
45
|
+
|
|
46
|
+
// Require each element of value.positiveArray be larger than zero, regardless of type
|
|
47
|
+
positiveArray: Arstotzka.ARRAY_OF(x => x > 0),
|
|
48
|
+
|
|
49
|
+
// Require value.nested to be an object
|
|
67
50
|
nested: {
|
|
68
|
-
parseableNumber
|
|
69
|
-
|
|
70
|
-
anotherNest
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
// Require value.nested.parseableNumber to make this function return true
|
|
52
|
+
parseableNumber: x => !isNaN(parseInt(x, 10)),
|
|
53
|
+
// Require value.nested.anotherNest to be an object AND make provided function return true
|
|
54
|
+
anotherNest: [{
|
|
55
|
+
phrase: "string",
|
|
56
|
+
wordCount: "number"
|
|
57
|
+
}, x => x.phrase.split(" ").length == x.wordCount],
|
|
58
|
+
// Require value.nested.justBeThere to be present
|
|
59
|
+
justBeThere: []
|
|
74
60
|
},
|
|
75
|
-
invalidValidator: "uhm"
|
|
76
|
-
};
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
one: ["number", x => x === 1]
|
|
87
|
-
}
|
|
88
|
-
];
|
|
89
|
-
const testSubject2 = {
|
|
90
|
-
type: 0,
|
|
91
|
-
zero: "0"
|
|
62
|
+
// Require value.optional to be of type "number", but only if present
|
|
63
|
+
optional: ["number", Arstotzka.OPTIONAL],
|
|
64
|
+
|
|
65
|
+
// Require value.booleanOrNumber to be either boolean, or 0 or 1
|
|
66
|
+
booleanOrNumber: Arstotzka.ANY_OF("boolean", x => x === 0 || x === 1),
|
|
67
|
+
|
|
68
|
+
// Require value.dynamic to be valid according to schema returned from provided callback (always "number" in that case)
|
|
69
|
+
dynamic: Arstotzka.DYNAMIC(x => "number")
|
|
92
70
|
};
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
72
|
+
// You can call validate() without providing any options. Below are all options with default values
|
|
73
|
+
const optionalValidationOptions = {
|
|
74
|
+
allowExtraProperties: true,
|
|
75
|
+
allErrors: true
|
|
76
|
+
};
|
|
98
77
|
|
|
99
|
-
const
|
|
100
|
-
[null, {x: "number"}],
|
|
101
|
-
[testSubject0, schema], // Only an error caused by invalid validator
|
|
102
|
-
[testSubject1, schema], // Full of errors
|
|
103
|
-
["miles", "string"], // Any value can be validated, not only objects
|
|
104
|
-
[7, "string"],
|
|
105
|
-
[testSubject2, schema1],
|
|
106
|
-
["7", parseableIntSchema]
|
|
107
|
-
];
|
|
78
|
+
const errors = Arstotzka.validate(value, schema, optionalValidationOptions);
|
|
108
79
|
|
|
109
|
-
|
|
110
|
-
console.log(Arstotzka.validate(tests[TEST_SELECTOR][0], tests[TEST_SELECTOR][1], {allowExtraProperties: false}));
|
|
80
|
+
console.log(errors); // Empty, meaning validation is passed
|