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