arstotzka 0.13.1 → 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 +37 -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();
@@ -75,34 +76,38 @@ function constraint(f, failMessageId, expected){
75
76
  };
76
77
  }
77
78
 
78
- function parseSchema(schemaProperty){
79
+ function parseSchema(schemaProperty, errors){
79
80
  function unify(raw){
80
- if (raw.type == CONSTRAINT) return raw;
81
-
82
- if (Array.isArray(raw)){
83
- return raw.map(c => unify(c));
84
- } else {
85
- switch (typeof raw){
86
- case ("string"): {
87
- if (raw == "array")
88
- return constraint(IS_ARRAY, "typeMismatch", raw);
89
- else
90
- return constraint(TYPE(raw), "typeMismatch", raw);
91
- }
92
- case ("function"): {
93
- return constraint(raw, "customFail", undefined);
94
- }
95
- case ("object"): {
96
- return constraint(FC_NESTED, null, raw);
97
- }
98
- case ("symbol"): {
99
- 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
+ }
100
103
  }
101
104
  }
102
105
  }
106
+ errors.push(error("<schema>", "parsingError", undefined, raw));
107
+ return null;
103
108
  }
104
109
 
105
- let intermediate = unify(schemaProperty);
110
+ let intermediate = unify(schemaProperty) || [];
106
111
  if (!Array.isArray(intermediate))
107
112
  intermediate = [intermediate];
108
113
  return [
@@ -135,7 +140,7 @@ function checkValue(propertyName, value, constraints, options){
135
140
  const schemaKeys = Object.keys(constraint.expected);
136
141
 
137
142
  for (let key of schemaKeys){
138
- const [subConstraints, flags] = parseSchema(constraint.expected[key]);
143
+ const [subConstraints, flags] = parseSchema(constraint.expected[key], errors);
139
144
 
140
145
  if (!targetKeys.includes(key)){
141
146
  if (!flags.includes(OPTIONAL)){
@@ -163,7 +168,7 @@ function checkValue(propertyName, value, constraints, options){
163
168
  break;
164
169
  }
165
170
 
166
- const [subConstraints, flags] = parseSchema(constraint.expected);
171
+ const [subConstraints, flags] = parseSchema(constraint.expected, errors);
167
172
 
168
173
  let indexCounter = 0;
169
174
  for (let item of value){
@@ -182,8 +187,10 @@ function checkValue(propertyName, value, constraints, options){
182
187
  let counter = 0;
183
188
 
184
189
  for (let subSchema of subSchemas){
185
- const [subConstraints, flags] = parseSchema(subSchema);
190
+ const subParseErrors = [];
191
+ const [subConstraints, flags] = parseSchema(subSchema, subParseErrors);
186
192
  const caseErrors = checkValue(`${propertyName}.<any#${counter}>`, value, subConstraints, options);
193
+ caseErrors.push(...subParseErrors);
187
194
  ++counter;
188
195
  subErrors.push(caseErrors);
189
196
  if (caseErrors.length == 0) {
@@ -204,7 +211,7 @@ function checkValue(propertyName, value, constraints, options){
204
211
  break;
205
212
  }
206
213
 
207
- const [subConstraints, flags] = parseSchema(constraintCallback(value))
214
+ const [subConstraints, flags] = parseSchema(constraintCallback(value), errors)
208
215
  const subErrors = checkValue(propertyName, value, subConstraints, options);
209
216
  errors.push(...subErrors);
210
217
 
@@ -227,14 +234,15 @@ function checkValue(propertyName, value, constraints, options){
227
234
  export function validate(target, schema = {}, options = {}){
228
235
  options = Object.assign(VALIDATION_DEFAULTS, options)
229
236
 
230
- const [constraints, flags] = parseSchema(schema);
237
+ const parseErrors = [];
238
+ const [constraints, flags] = parseSchema(schema, parseErrors);
231
239
 
232
240
  if (flags.length > 0) {
233
241
  console.error("Flags can't be used at schema's root")
234
242
  }
235
243
 
236
244
  const errors = checkValue("", target, constraints, options);
237
-
245
+ errors.push(...parseErrors)
238
246
  errors.forEach(e => {
239
247
  if (e.propertyName?.startsWith("."))
240
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.1",
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, "array"],
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