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.
Files changed (4) hide show
  1. package/index.js +38 -29
  2. package/package.json +2 -2
  3. package/tests.js +115 -0
  4. 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.type == CONSTRAINT) return raw;
80
-
81
- if (Array.isArray(raw)){
82
- return raw.map(c => unify(c));
83
- } else {
84
- switch (typeof raw){
85
- case ("string"): {
86
- if (raw == "array")
87
- return constraint(IS_ARRAY, "typeMismatch", raw);
88
- else
89
- return constraint(TYPE(raw), "typeMismatch", raw);
90
- }
91
- case ("function"): {
92
- return constraint(raw, "customFail", undefined);
93
- }
94
- case ("object"): {
95
- return constraint(FC_NESTED, null, raw);
96
- }
97
- case ("symbol"): {
98
- return raw;
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 [subConstraints, flags] = parseSchema(subSchema);
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 [constraints, flags] = parseSchema(schema);
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.0",
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": "echo \"Error: no test specified\" && exit 1"
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
- 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 = {
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
- notArray: [],
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: 1
21
+ phrase:"henlo there",
22
+ wordCount: 2
45
23
  },
46
- missing: 0
24
+ justBeThere: 0
47
25
  },
48
- invalidValidator: "uhm"
26
+ booleanOrNumber: 0,
27
+ dynamic: 69
49
28
  };
50
29
 
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],
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: "seven",
69
- extraProperty: "hey",
70
- anotherNest: {
71
- phrase:"henlo",
72
- wordCount: 2
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
- 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"
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
- const parseableIntSchema = Arstotzka.ANY_OF(
95
- "number",
96
- ["string", x => !isNaN(parseInt(x))]
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 tests = [
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
- const TEST_SELECTOR = 0;
110
- console.log(Arstotzka.validate(tests[TEST_SELECTOR][0], tests[TEST_SELECTOR][1], {allowExtraProperties: false}));
80
+ console.log(errors); // Empty, meaning validation is passed