arstotzka 0.11.1 → 0.12.0
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 +138 -123
- package/package.json +4 -3
- package/readme.md +26 -20
- package/usage.js +31 -7
package/index.js
CHANGED
|
@@ -4,32 +4,34 @@ export const ERRORS = {
|
|
|
4
4
|
customFail: "Custom validation function failed",
|
|
5
5
|
extraProperty: "Provided object contains properties not present in schema",
|
|
6
6
|
exceptionOnCustom: "Exception thrown during constraint validation",
|
|
7
|
-
notArray: "Tried using ARRAY_OF constraint on non-array value"
|
|
7
|
+
notArray: "Tried using ARRAY_OF constraint on non-array value",
|
|
8
|
+
targetIsNull: "Passed object or array item is null",
|
|
9
|
+
functionExpected: "Expected function as dynamic constraint",
|
|
10
|
+
objectExpected: "Expected object"
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
export const OPTIONAL = Symbol();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export function ARRAY_OF(constraints){
|
|
15
|
+
if (arguments.length > 1){
|
|
16
|
+
console.error("Got more than one argument to ARRAY_OF. Did you mean to pass an array of constraints?");
|
|
17
|
+
}
|
|
18
|
+
return constraint(FC_ARRAY, null, constraints);
|
|
19
|
+
}
|
|
20
|
+
export function DYNAMIC(constraints){
|
|
21
|
+
return constraint(FC_DYNAMIC, null, constraints);
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const TYPE = t => x => typeof x == t;
|
|
21
|
-
export const NOT_NULL = x => x !== null;
|
|
22
|
-
export const IS_ARRAY = x => Array.isArray(x);
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
26
|
+
const CONSTRAINT = Symbol();
|
|
27
|
+
const FC_ARRAY = Symbol(); // https://www.youtube.com/watch?v=qSqXGeJJBaI
|
|
28
|
+
const FC_DYNAMIC = Symbol();
|
|
29
|
+
const FC_NESTED = Symbol();
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const VALIDATION_DEFAULTS = {
|
|
32
|
+
allErrors: true,
|
|
33
|
+
allowExtraProperties: true
|
|
34
|
+
};
|
|
33
35
|
|
|
34
36
|
function safe(cb){
|
|
35
37
|
try {
|
|
@@ -49,144 +51,157 @@ function error(propertyName, id, expected, got){
|
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const VALIDATION_DEFAULTS = {
|
|
53
|
-
allErrors: true,
|
|
54
|
-
allowExtraProperties: true,
|
|
55
|
-
selfAlias: "_self"
|
|
56
|
-
};
|
|
57
|
-
|
|
58
54
|
function constraint(f, failMessageId, expected){
|
|
59
55
|
return {
|
|
56
|
+
type: CONSTRAINT,
|
|
60
57
|
validation: f,
|
|
61
58
|
failMessageId: failMessageId,
|
|
62
59
|
expected: expected
|
|
63
60
|
};
|
|
64
61
|
}
|
|
65
62
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return raw;
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
63
|
+
function parseSchema(schemaProperty){
|
|
64
|
+
function unify(raw){
|
|
65
|
+
if (raw.type == CONSTRAINT) return raw;
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(raw)){
|
|
68
|
+
return raw.map(c => unify(c));
|
|
69
|
+
} else {
|
|
70
|
+
switch (typeof raw){
|
|
71
|
+
case ("string"): {
|
|
72
|
+
if (raw == "array")
|
|
73
|
+
return constraint(IS_ARRAY, "typeMismatch", raw);
|
|
74
|
+
else
|
|
75
|
+
return constraint(TYPE(raw), "typeMismatch", raw);
|
|
76
|
+
}
|
|
77
|
+
case ("function"): {
|
|
78
|
+
return constraint(raw, "customFail", undefined);
|
|
79
|
+
}
|
|
80
|
+
case ("object"): {
|
|
81
|
+
return constraint(FC_NESTED, null, raw);
|
|
82
|
+
}
|
|
83
|
+
case ("symbol"): {
|
|
84
|
+
return raw;
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
|
-
} else {
|
|
81
|
-
return [raw];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function stricterConstraints(raw){
|
|
87
|
-
const r = [];
|
|
88
|
-
for (let c of raw){
|
|
89
|
-
if (isForbidden(c)) continue;
|
|
90
|
-
switch (typeof c){
|
|
91
|
-
case ("string"): {
|
|
92
|
-
if (c == "array")
|
|
93
|
-
r.push(constraint(IS_ARRAY, "typeMismatch", c))
|
|
94
|
-
else
|
|
95
|
-
r.push(constraint(TYPE(c), "typeMismatch", c))
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
case ("function"): {
|
|
99
|
-
r.push(constraint(c, "customFail", undefined));
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
87
|
}
|
|
103
88
|
}
|
|
104
|
-
|
|
89
|
+
|
|
90
|
+
let intermediate = unify(schemaProperty);
|
|
91
|
+
if (!Array.isArray(intermediate))
|
|
92
|
+
intermediate = [intermediate];
|
|
93
|
+
return [
|
|
94
|
+
intermediate.filter(i => i.type == CONSTRAINT),
|
|
95
|
+
intermediate.filter(i => i.type != CONSTRAINT && typeof i == "symbol")
|
|
96
|
+
];
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
* @param options Validation options:
|
|
109
|
-
*
|
|
110
|
-
* - allErrors (true) : return all errors instead of interrupting after first fail
|
|
111
|
-
* - allowExtraProperties (true) : If false, adds specific error to a list for every property of target object not present in schema
|
|
112
|
-
* - selfAlias ("_self"): Schema property name for referring nested object itself
|
|
113
|
-
* @return Array of errors
|
|
114
|
-
*/
|
|
115
|
-
export function validate(target, schema = {}, options = {}){
|
|
116
|
-
options = Object.assign(VALIDATION_DEFAULTS, options)
|
|
99
|
+
function checkValue(propertyName, value, constraints, options){
|
|
117
100
|
const errors = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (let sKey of schemaKeys){
|
|
123
|
-
if (!options.allErrors && errors.length > 0)
|
|
124
|
-
return errors;
|
|
125
|
-
|
|
126
|
-
const rawPropertySchema = schema[sKey];
|
|
127
|
-
const schemaAsArray = arrayOfConstraints(rawPropertySchema, options.selfAlias);
|
|
128
|
-
const flags = schemaAsArray.filter(c => typeof c == "symbol");
|
|
129
|
-
const constraints = stricterConstraints(schemaAsArray);
|
|
130
|
-
const forbiddenConstraints = schemaAsArray.filter(s => isForbidden(s));
|
|
131
|
-
|
|
132
|
-
if (!targetKeys.includes(sKey)){
|
|
133
|
-
if (!flags.includes(OPTIONAL)){
|
|
134
|
-
errors.push(error(sKey, "noProperty"));
|
|
135
|
-
}
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (let constraint of constraints){
|
|
140
|
-
const [success, validationResult] = safe(() => constraint.validation(target[sKey]));
|
|
101
|
+
for (let constraint of constraints){
|
|
102
|
+
if (typeof constraint.validation == "function"){
|
|
103
|
+
const [success, validationResult] = safe(() => constraint.validation(value));
|
|
141
104
|
if (!success){
|
|
142
|
-
errors.push(error(
|
|
105
|
+
errors.push(error(propertyName, "exceptionOnCustom", undefined, validationResult.toString()));
|
|
143
106
|
} else if (!validationResult){
|
|
144
|
-
errors.push(error(
|
|
107
|
+
errors.push(error(propertyName, constraint.failMessageId, constraint.expected, value));
|
|
145
108
|
}
|
|
146
109
|
continue;
|
|
147
110
|
}
|
|
148
111
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
112
|
+
switch(constraint.validation){
|
|
113
|
+
case FC_NESTED: {
|
|
114
|
+
if (typeof value != "object" || value === null || value == undefined){
|
|
115
|
+
errors.push(error(propertyName, "objectExpected", "object", value));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const targetKeys = Object.keys(value);
|
|
120
|
+
const schemaKeys = Object.keys(constraint.expected);
|
|
121
|
+
|
|
122
|
+
for (let key of schemaKeys){
|
|
123
|
+
const [subConstraints, flags] = parseSchema(constraint.expected[key]);
|
|
124
|
+
|
|
125
|
+
if (!targetKeys.includes(key)){
|
|
126
|
+
if (!flags.includes(OPTIONAL)){
|
|
127
|
+
errors.push(error(`${propertyName}.${key}`, "noProperty"));
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
160
130
|
}
|
|
161
131
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
132
|
+
const subErrors = checkValue(`${propertyName}.${key}`, value[key], subConstraints, options);
|
|
133
|
+
errors.push(...subErrors);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!options.allowExtraProperties){
|
|
137
|
+
const extraProperties = targetKeys.filter(k => !schemaKeys.includes(k));
|
|
138
|
+
if (extraProperties.length > 0){
|
|
139
|
+
errors.push(...extraProperties.map(k => error(`${propertyName}.${k}`, "extraProperty")))
|
|
140
|
+
}
|
|
165
141
|
}
|
|
166
|
-
|
|
142
|
+
|
|
167
143
|
break;
|
|
168
144
|
}
|
|
145
|
+
case FC_ARRAY: {
|
|
146
|
+
if (!Array.isArray(value)){
|
|
147
|
+
errors.push(error(propertyName, "notArray", "array", typeof value))
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const [subConstraints, flags] = parseSchema(constraint.expected);
|
|
152
|
+
|
|
153
|
+
let indexCounter = 0;
|
|
154
|
+
for (let item of value){
|
|
155
|
+
const subErrors = checkValue(`${propertyName}[${indexCounter}]`, item, subConstraints, options);
|
|
156
|
+
errors.push(...subErrors);
|
|
157
|
+
++indexCounter;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
break;
|
|
169
161
|
}
|
|
170
|
-
|
|
162
|
+
case FC_DYNAMIC: {
|
|
163
|
+
const constraintCallback = constraint.expected;
|
|
164
|
+
|
|
165
|
+
if (typeof constraintCallback != "function"){
|
|
166
|
+
errors.push(error(propertyName, "functionExpected", "function", typeof constraintCallback))
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const [subConstraints, flags] = parseSchema(constraintCallback(value))
|
|
171
|
+
const subErrors = checkValue(propertyName, value, subConstraints, options);
|
|
172
|
+
errors.push(...subErrors);
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
e.propertyName = `${sKey}.${e.propertyName}`;
|
|
178
|
-
return e;
|
|
179
|
-
});
|
|
180
|
-
errors.push(...mappedErrors);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
181
176
|
}
|
|
177
|
+
|
|
178
|
+
if (!options.allErrors && errors.length > 0) break;
|
|
182
179
|
}
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
183
182
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
/**
|
|
184
|
+
* @param options Validation options:
|
|
185
|
+
*
|
|
186
|
+
* - allErrors (true) : return all errors instead of interrupting after first fail
|
|
187
|
+
* - allowExtraProperties (true) : If false, adds specific error to a list for every property of target object not present in schema
|
|
188
|
+
* @return Array of errors
|
|
189
|
+
*/
|
|
190
|
+
export function validate(target, schema = {}, options = {}){
|
|
191
|
+
options = Object.assign(VALIDATION_DEFAULTS, options)
|
|
192
|
+
|
|
193
|
+
const [constraints, flags] = parseSchema(schema);
|
|
194
|
+
|
|
195
|
+
if (flags.length > 0) {
|
|
196
|
+
console.error("Flags can't be used at schema's root")
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
const errors = checkValue("", target, constraints, options);
|
|
200
|
+
|
|
201
|
+
errors.forEach(e => {
|
|
202
|
+
if (e.propertyName?.startsWith("."))
|
|
203
|
+
e.propertyName = e.propertyName.slice(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
191
206
|
return errors;
|
|
192
207
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arstotzka",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "JS validation utility",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
"keywords": [
|
|
11
11
|
"JSON",
|
|
12
12
|
"schema",
|
|
13
|
+
"data",
|
|
13
14
|
"validator",
|
|
14
15
|
"validation",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
16
|
+
"data-validation",
|
|
17
|
+
"schema-validation"
|
|
17
18
|
],
|
|
18
19
|
"author": "milesseventh",
|
|
19
20
|
"license": "WTFPL",
|
package/readme.md
CHANGED
|
@@ -27,35 +27,40 @@ const goodData = {
|
|
|
27
27
|
]
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
console.log(Arstotzka.validate(goodData, schema, {allowExtraProperties: false}));
|
|
30
|
+
console.log(Arstotzka.validate(goodData, schema, {allowExtraProperties: false})); // Prints an array of errors
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### Import:
|
|
34
|
-
|
|
34
|
+
Arstotzka is an ES6 module, so:
|
|
35
|
+
`import * as Arstotzka from "arstotzka";`
|
|
36
|
+
or, if you are okay with polluting namespace:
|
|
37
|
+
`import { validate, OPTIONAL, ARRAY_OF, DYNAMIC } from "arstotzka";`
|
|
35
38
|
|
|
36
39
|
### Schema format:
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
- `
|
|
52
|
-
|
|
53
|
-
-
|
|
40
|
+
Schema is a value that can be a
|
|
41
|
+
- **string**: such schema will make validator check coresponding value's type with [typeof operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof), with exception of "array" constraint -- validator will [treat this type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray) as special case;
|
|
42
|
+
`"string"`, `"number"`, `"array"`
|
|
43
|
+
|
|
44
|
+
- **function**: such schema will make validator call it, pass corresponding value to it, and log an error if returned value is falsy;
|
|
45
|
+
`x => !isNaN(x)`, `x => x.length < 10`
|
|
46
|
+
|
|
47
|
+
- **object**: object schema requires corresponding value to also be an object, and will recursively match it's properties against provided value;
|
|
48
|
+
`{name: "string", age: x => x.length > 21}`
|
|
49
|
+
|
|
50
|
+
- **array**, which elements are any of above. That will require a value it matched against to fullfill *every* requirement;
|
|
51
|
+
`["string", x => x != x.trim()]`, `["number", x => x >= 0, x => x % 1 === 0]`
|
|
52
|
+
|
|
53
|
+
- **Arstotzka.ARRAY_OF()**: the function accepts any of above and returns a special constraint appliable to an array of values;
|
|
54
|
+
- `Arstotzka.ARRAY_OF("number")`, `Arstotzka.ARRAY_OF({id: "number", text: "string"})`, `Arstotzka.ARRAY_OF(["number", x => x > 0])`, `Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number"))`
|
|
55
|
+
|
|
56
|
+
- **Arstotzka.DYNAMIC()**: the function accepts a callback that should return a valid schema, allowing to define schema at runtime;
|
|
57
|
+
- `Arstotzka.DYNAMIC(x => dynSchema[x.type])`
|
|
58
|
+
|
|
59
|
+
Applying a schema to a property that is an object can be done by combining **object** schema with anything via **array** schema:
|
|
54
60
|
|
|
55
61
|
### Available options:
|
|
56
62
|
- **allErrors** (default is `true`) : If false, will return errors as soon as encountered, interrupting validation
|
|
57
63
|
- **allowExtraProperties** (default is `true`) : If false, adds specific error to a list for every property of target object not present in schema
|
|
58
|
-
- **selfAlias** (default is `"_self"`): Schema property name for referring nested object itself
|
|
59
64
|
|
|
60
65
|
### Error format
|
|
61
66
|
```
|
|
@@ -67,6 +72,7 @@ console.log(Arstotzka.validate(goodData, schema, {allowExtraProperties: false}))
|
|
|
67
72
|
got: null
|
|
68
73
|
}
|
|
69
74
|
```
|
|
75
|
+
|
|
70
76
|
All error ids and messages can be found at `Arstotzka.ERRORS`
|
|
71
77
|
|
|
72
78
|
## License
|
package/usage.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as Arstotzka from "./index.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
const schema = {
|
|
5
4
|
title: "string",
|
|
6
5
|
array: [Arstotzka.ARRAY_OF("number"), x => x.length > 1],
|
|
@@ -13,11 +12,10 @@ const schema = {
|
|
|
13
12
|
positiveArray: Arstotzka.ARRAY_OF(x => x > 0),
|
|
14
13
|
nested: {
|
|
15
14
|
parseableNumber: [x => !isNaN(parseInt(x, 10))],
|
|
16
|
-
anotherNest: {
|
|
17
|
-
_self: [x => x.phrase.split(" ").length == x.wordCount],
|
|
15
|
+
anotherNest: [{
|
|
18
16
|
phrase: "string",
|
|
19
17
|
wordCount: "number"
|
|
20
|
-
},
|
|
18
|
+
}, x => x.phrase.split(" ").length == x.wordCount],
|
|
21
19
|
missing: []
|
|
22
20
|
},
|
|
23
21
|
invalidValidator: [x => null.invalid],
|
|
@@ -48,7 +46,7 @@ const testSubject0 = {
|
|
|
48
46
|
missing: 0
|
|
49
47
|
},
|
|
50
48
|
invalidValidator: "uhm"
|
|
51
|
-
}
|
|
49
|
+
};
|
|
52
50
|
|
|
53
51
|
const testSubject1 = {
|
|
54
52
|
title: 1337,
|
|
@@ -75,6 +73,32 @@ const testSubject1 = {
|
|
|
75
73
|
}
|
|
76
74
|
},
|
|
77
75
|
invalidValidator: "uhm"
|
|
78
|
-
}
|
|
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 tests = [
|
|
95
|
+
[null, {x: "number"}],
|
|
96
|
+
[testSubject0, schema], // Only an error caused by invalid validator
|
|
97
|
+
[testSubject1, schema], // Full of errors
|
|
98
|
+
["miles", "string"], // Any value can be validated, not only objects
|
|
99
|
+
[7, "string"],
|
|
100
|
+
[testSubject2, schema1]
|
|
101
|
+
];
|
|
79
102
|
|
|
80
|
-
|
|
103
|
+
const TEST_SELECTOR = 0;
|
|
104
|
+
console.log(Arstotzka.validate(tests[TEST_SELECTOR][0], tests[TEST_SELECTOR][1], {allowExtraProperties: false}));
|