arstotzka 0.11.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 +192 -0
- package/license.md +15 -0
- package/package.json +28 -0
- package/readme.md +48 -0
- package/usage.js +80 -0
package/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
export const ERRORS = {
|
|
2
|
+
noProperty: "Required property not present",
|
|
3
|
+
typeMismatch: "Provided type is not allowed by schema",
|
|
4
|
+
customFail: "Custom validation function failed",
|
|
5
|
+
extraProperty: "Provided object contains properties not present in schema",
|
|
6
|
+
exceptionOnCustom: "Exception thrown during constraint validation",
|
|
7
|
+
notArray: "Tried using ARRAY_OF constraint on non-array value"
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const OPTIONAL = Symbol();
|
|
11
|
+
const FORBIDDEN_SIGNATURE = Symbol(); // https://www.youtube.com/watch?v=qSqXGeJJBaI
|
|
12
|
+
const FC_ARRAY = Symbol();
|
|
13
|
+
export function ARRAY_OF(rawConstraints){
|
|
14
|
+
const proto = forbiddenObject();
|
|
15
|
+
proto.type = FC_ARRAY;
|
|
16
|
+
proto.rawConstraints = rawConstraints;
|
|
17
|
+
return proto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
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
|
+
|
|
24
|
+
function forbiddenObject(){
|
|
25
|
+
return {
|
|
26
|
+
forbiddenKey: FORBIDDEN_SIGNATURE
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isForbidden(obj){
|
|
31
|
+
return obj?.forbiddenKey == FORBIDDEN_SIGNATURE;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safe(cb){
|
|
35
|
+
try {
|
|
36
|
+
return [true, cb()];
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return [false, e];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function error(propertyName, id, expected, got){
|
|
43
|
+
return {
|
|
44
|
+
propertyName,
|
|
45
|
+
id,
|
|
46
|
+
message: ERRORS[id],
|
|
47
|
+
expected,
|
|
48
|
+
got
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const VALIDATION_DEFAULTS = {
|
|
53
|
+
allErrors: true,
|
|
54
|
+
allowExtraProperties: true,
|
|
55
|
+
selfAlias: "_self"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function constraint(f, failMessageId, expected){
|
|
59
|
+
return {
|
|
60
|
+
validation: f,
|
|
61
|
+
failMessageId: failMessageId,
|
|
62
|
+
expected: expected
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function arrayOfConstraints(raw, selfAlias){
|
|
67
|
+
if (Array.isArray(raw)){
|
|
68
|
+
return raw;
|
|
69
|
+
} else {
|
|
70
|
+
if (typeof raw == "object"){
|
|
71
|
+
if (isForbidden(raw)){
|
|
72
|
+
return [raw]
|
|
73
|
+
} else {
|
|
74
|
+
if (raw.hasOwnProperty(selfAlias)){
|
|
75
|
+
return arrayOfConstraints(raw[selfAlias]);
|
|
76
|
+
} else {
|
|
77
|
+
return ["object"];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return r;
|
|
105
|
+
}
|
|
106
|
+
|
|
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)
|
|
117
|
+
const errors = [];
|
|
118
|
+
|
|
119
|
+
const targetKeys = Object.keys(target);
|
|
120
|
+
const schemaKeys = Object.keys(schema);
|
|
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]));
|
|
141
|
+
if (!success){
|
|
142
|
+
errors.push(error(sKey, "exceptionOnCustom", undefined, validationResult.toString()));
|
|
143
|
+
} else if (!validationResult){
|
|
144
|
+
errors.push(error(sKey, constraint.failMessageId, constraint.expected, target[sKey]));
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (let fc of forbiddenConstraints){
|
|
150
|
+
switch (fc.type){
|
|
151
|
+
case FC_ARRAY: {
|
|
152
|
+
if (Array.isArray(target[sKey])){
|
|
153
|
+
const forbiddenErrors = [];
|
|
154
|
+
let indexCounter = 0;
|
|
155
|
+
for (let item of target[sKey]){
|
|
156
|
+
const e = validate({"": item}, {"": fc.rawConstraints}, options);
|
|
157
|
+
e.forEach(err => err.propertyName = `${sKey}[${indexCounter}]${err.propertyName}`);
|
|
158
|
+
forbiddenErrors.push(...e);
|
|
159
|
+
++indexCounter;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
errors.push(...forbiddenErrors);
|
|
163
|
+
} else {
|
|
164
|
+
errors.push(error(sKey, "notArray", "array", typeof target[sKey]))
|
|
165
|
+
}
|
|
166
|
+
const success = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof rawPropertySchema == "object" && !Array.isArray(rawPropertySchema) && !isForbidden(rawPropertySchema)){
|
|
173
|
+
delete rawPropertySchema[options.selfAlias];
|
|
174
|
+
|
|
175
|
+
const nestedErrors = validate(target[sKey], rawPropertySchema, options);
|
|
176
|
+
const mappedErrors = nestedErrors.map(e => {
|
|
177
|
+
e.propertyName = `${sKey}.${e.propertyName}`;
|
|
178
|
+
return e;
|
|
179
|
+
});
|
|
180
|
+
errors.push(...mappedErrors);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!options.allowExtraProperties){
|
|
185
|
+
const extraProperties = targetKeys.filter(k => !schemaKeys.includes(k));
|
|
186
|
+
if (extraProperties.length > 0){
|
|
187
|
+
errors.push(...extraProperties.map(k => error(k, "extraProperty")))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return errors;
|
|
192
|
+
}
|
package/license.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
2
|
+
Version 2, December 2004
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
|
5
|
+
|
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
|
7
|
+
copies of this license document, and changing it is allowed as long
|
|
8
|
+
as the name is changed.
|
|
9
|
+
|
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
12
|
+
|
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
14
|
+
|
|
15
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arstotzka",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "JS validation utility",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"JSON",
|
|
12
|
+
"schema",
|
|
13
|
+
"validator",
|
|
14
|
+
"validation",
|
|
15
|
+
"jsonschema",
|
|
16
|
+
"json-schema"
|
|
17
|
+
],
|
|
18
|
+
"author": "milesseventh",
|
|
19
|
+
"license": "WTFPL",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/MilesVII/arstotzka.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/MilesVII/arstotzka/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/MilesVII/arstotzka#readme"
|
|
28
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Arstotzka
|
|
2
|
+
JS data validation tool featuring laconic schema format and a sexy name.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
|
|
6
|
+
See detailed usage example in **[usage.js](https://github.com/MilesVII/arstotzka/blob/master/usage.js)**
|
|
7
|
+
|
|
8
|
+
### Import:
|
|
9
|
+
`import * as Arstotzka from "Arstotzka";`
|
|
10
|
+
|
|
11
|
+
### Schema format:
|
|
12
|
+
- You build a schema, an object describing requirements for each property of object you wish to validate, then pass target object, that schema and options to `validate()` function and receive an array of detailed errors in return.
|
|
13
|
+
- `const arrayOfErrors = Arstotzka.validate(objectToCheck, schema, options);`
|
|
14
|
+
- Aforemetioned requirements are called **constraints**.
|
|
15
|
+
- Plain string constraints check porperty'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.
|
|
16
|
+
- Examples: `name: "string"`, `id: "number"`, `list: "array"`
|
|
17
|
+
- You can also specify custom constraint by providing a validation function. Validator will pass property value to it and will add an error if returned value is falsy. Exception thrown inside validation faction will be catched and added as errors into output
|
|
18
|
+
- Examples: `positiveNumber: x => x > 0`, `nickname: x => x.length < 10`
|
|
19
|
+
- It is possible to validate nested objects by passing a schema object instead of constraint:
|
|
20
|
+
- `user: {name: "string", age: "number"}`
|
|
21
|
+
- If needed, you can still add constraints to the nested object itself, passing them to it's `_self` property. Name of the property can be changed in the options.
|
|
22
|
+
- `phrase: {_self: x => x.text.length == x.letterCount, text: "string", letterCount: "number"}`
|
|
23
|
+
- You can combine different constraints by passing an array of them.
|
|
24
|
+
- Just like that: `nickname: ["string", x => x.length < 10]`, `count: ["number", x => x >= 0, x => x % 1 == 0]`
|
|
25
|
+
- There are special constraints that serve as flags. (The only) one of them is **Arstotzka.OPTIONAL**. It allows to validate a property but prevents validator from adding an error if that property is not present in target object.
|
|
26
|
+
- `commentary: ["string", Arstotzka.OPTIONAL]`
|
|
27
|
+
- Finally, you can apply constraints to array's elements with **Arstotzka.ARRAY_OF()**. All the constraints passed to that function will be applied for each element.
|
|
28
|
+
- `numberArray: Arstotzka.ARRAY_OF("number")`, `posts: Arstotzka.ARRAY_OF({id: "number", text: "string"})`, `positives: Arstotzka.ARRAY_OF(["number", x = x > 0])`, or even `matrix: Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number"))`
|
|
29
|
+
|
|
30
|
+
### Available options:
|
|
31
|
+
- **allErrors** (default is `true`) : If false, will return errors as soon as encountered, interrupting validation
|
|
32
|
+
- **allowExtraProperties** (default is `true`) : If false, adds specific error to a list for every property of target object not present in schema
|
|
33
|
+
- **selfAlias** (default is `"_self"`): Schema property name for referring nested object itself
|
|
34
|
+
|
|
35
|
+
### Error format
|
|
36
|
+
```
|
|
37
|
+
{ // Example error item:
|
|
38
|
+
propertyName: 'age', // Name of a property that failed validation
|
|
39
|
+
id: 'typeMismatch', // String describing type of an error. Can be used to localize error message
|
|
40
|
+
message: 'Provided type is not allowed by schema', // Error message that coressponds to error id
|
|
41
|
+
expected: 'number', // Arbitrary-purpose fields
|
|
42
|
+
got: null
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
All error ids and messages can be found at `Arstotzka.ERRORS`
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
Shared under WTFPL license.
|
package/usage.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as Arstotzka from "./index.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const schema = {
|
|
5
|
+
title: "string",
|
|
6
|
+
array: [Arstotzka.ARRAY_OF("number"), x => x.length > 1],
|
|
7
|
+
arrayOfObjs: Arstotzka.ARRAY_OF({
|
|
8
|
+
id: "number",
|
|
9
|
+
name: "string"
|
|
10
|
+
}),
|
|
11
|
+
notArray: Arstotzka.ARRAY_OF("string"),
|
|
12
|
+
arrayOof: Arstotzka.ARRAY_OF(Arstotzka.ARRAY_OF("number")),
|
|
13
|
+
positiveArray: Arstotzka.ARRAY_OF(x => x > 0),
|
|
14
|
+
nested: {
|
|
15
|
+
parseableNumber: [x => !isNaN(parseInt(x, 10))],
|
|
16
|
+
anotherNest: {
|
|
17
|
+
_self: [x => x.phrase.split(" ").length == x.wordCount],
|
|
18
|
+
phrase: "string",
|
|
19
|
+
wordCount: "number"
|
|
20
|
+
},
|
|
21
|
+
missing: []
|
|
22
|
+
},
|
|
23
|
+
invalidValidator: [x => null.invalid],
|
|
24
|
+
optional: ["number", Arstotzka.OPTIONAL]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const testSubject0 = {
|
|
28
|
+
title: "1337",
|
|
29
|
+
array: [1, 2, 3],
|
|
30
|
+
arrayOfObjs: [
|
|
31
|
+
{id: 0, name: "_"},
|
|
32
|
+
{id: 1, name: "second"},
|
|
33
|
+
{id: 2, name: "3"},
|
|
34
|
+
],
|
|
35
|
+
notArray: [],
|
|
36
|
+
arrayOof: [
|
|
37
|
+
[1, 2, 3],
|
|
38
|
+
[4, 5, 6],
|
|
39
|
+
[1, 2]
|
|
40
|
+
],
|
|
41
|
+
positiveArray: [Infinity, 1, 4, 5],
|
|
42
|
+
nested: {
|
|
43
|
+
parseableNumber: "777",
|
|
44
|
+
anotherNest: {
|
|
45
|
+
phrase:"henlo",
|
|
46
|
+
wordCount: 1
|
|
47
|
+
},
|
|
48
|
+
missing: 0
|
|
49
|
+
},
|
|
50
|
+
invalidValidator: "uhm"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const testSubject1 = {
|
|
54
|
+
title: 1337,
|
|
55
|
+
array: [1, null, 3],
|
|
56
|
+
arrayOfObjs: [
|
|
57
|
+
{id: 0, name: "_"},
|
|
58
|
+
{id: 0,},
|
|
59
|
+
{id: "0", name: "_"}
|
|
60
|
+
],
|
|
61
|
+
notArray: "[1, 2, 3]",
|
|
62
|
+
arrayOof: [
|
|
63
|
+
[1, 2, 3],
|
|
64
|
+
[4, "5", 6],
|
|
65
|
+
null,
|
|
66
|
+
[1, 2]
|
|
67
|
+
],
|
|
68
|
+
positiveArray: [0, 1, 4, -5],
|
|
69
|
+
nested: {
|
|
70
|
+
parseableNumber: "seven",
|
|
71
|
+
extraProperty: "hey",
|
|
72
|
+
anotherNest: {
|
|
73
|
+
phrase:"henlo",
|
|
74
|
+
wordCount: 2
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
invalidValidator: "uhm"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(Arstotzka.validate(testSubject1, schema, {allowExtraProperties: false}));
|