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.
Files changed (4) hide show
  1. package/index.js +138 -123
  2. package/package.json +4 -3
  3. package/readme.md +26 -20
  4. 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
- 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;
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
- function forbiddenObject(){
25
- return {
26
- forbiddenKey: FORBIDDEN_SIGNATURE
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
- function isForbidden(obj){
31
- return obj?.forbiddenKey == FORBIDDEN_SIGNATURE;
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 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"];
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
- return r;
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
- 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]));
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(sKey, "exceptionOnCustom", undefined, validationResult.toString()));
105
+ errors.push(error(propertyName, "exceptionOnCustom", undefined, validationResult.toString()));
143
106
  } else if (!validationResult){
144
- errors.push(error(sKey, constraint.failMessageId, constraint.expected, target[sKey]));
107
+ errors.push(error(propertyName, constraint.failMessageId, constraint.expected, value));
145
108
  }
146
109
  continue;
147
110
  }
148
111
 
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;
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
- errors.push(...forbiddenErrors);
163
- } else {
164
- errors.push(error(sKey, "notArray", "array", typeof target[sKey]))
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
- const success = true;
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
- if (typeof rawPropertySchema == "object" && !Array.isArray(rawPropertySchema) && !isForbidden(rawPropertySchema)){
173
- delete rawPropertySchema[options.selfAlias];
170
+ const [subConstraints, flags] = parseSchema(constraintCallback(value))
171
+ const subErrors = checkValue(propertyName, value, subConstraints, options);
172
+ errors.push(...subErrors);
174
173
 
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);
174
+ break;
175
+ }
181
176
  }
177
+
178
+ if (!options.allErrors && errors.length > 0) break;
182
179
  }
180
+ return errors;
181
+ }
183
182
 
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
- }
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.11.1",
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
- "jsonschema",
16
- "json-schema"
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
- `import * as Arstotzka from "Arstotzka";`
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
- - 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.
38
- - `const arrayOfErrors = Arstotzka.validate(objectToCheck, schema, options);`
39
- - Aforemetioned requirements are called **constraints**.
40
- - 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.
41
- - Examples: `name: "string"`, `id: "number"`, `list: "array"`
42
- - 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
43
- - Examples: `positiveNumber: x => x > 0`, `nickname: x => x.length < 10`
44
- - It is possible to validate nested objects by passing a schema object instead of constraint:
45
- - `user: {name: "string", age: "number"}`
46
- - 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.
47
- - `phrase: {_self: x => x.text.length == x.letterCount, text: "string", letterCount: "number"}`
48
- - You can combine different constraints by passing an array of them.
49
- - Just like that: `nickname: ["string", x => x.length < 10]`, `count: ["number", x => x >= 0, x => x % 1 == 0]`
50
- - 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.
51
- - `commentary: ["string", Arstotzka.OPTIONAL]`
52
- - 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.
53
- - `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"))`
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
- console.log(Arstotzka.validate(testSubject1, schema, {allowExtraProperties: false}));
103
+ const TEST_SELECTOR = 0;
104
+ console.log(Arstotzka.validate(tests[TEST_SELECTOR][0], tests[TEST_SELECTOR][1], {allowExtraProperties: false}));