schiva 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,20 +1,56 @@
1
1
  # schiva
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/schiva.svg)](https://www.npmjs.com/package/schiva)
4
+ [![CI](https://github.com/jesus-seijas-sp/schiva/actions/workflows/ci.yml/badge.svg)](https://github.com/jesus-seijas-sp/schiva/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](package.json)
7
+
3
8
  Fast schema validation for JavaScript. Describe data with a small schema DSL or with JSON Schema (draft-07), and
4
9
  schiva compiles it into a JavaScript function generated for that schema, so validating is several times faster than
5
10
  walking the schema for every value.
6
11
 
7
- - Passes the whole draft-07 [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
8
- (929 of 929 tests), including `$ref` to other documents.
9
- - Readable error messages with the path of each field: `lines[12].price must be a number`.
10
- - Three modes: every error, the first error only, or just `true`/`false`.
11
- - No dependencies. Unknown or unsupported keywords throw instead of being silently ignored.
12
+ **Documentation: [schiva.js.org](https://schiva.js.org)**
13
+
14
+ ## Contents
15
+
16
+ - [Features](#features)
17
+ - [Install](#install)
18
+ - [Getting started](#getting-started)
19
+ - [Modes](#modes)
20
+ - [Compile once](#compile-once)
21
+ - [Schema DSL](#schema-dsl)
22
+ - [JSON Schema](#json-schema)
23
+ - [Errors](#errors)
24
+ - [Types of your own](#types-of-your-own)
25
+ - [Security considerations](#security-considerations)
26
+ - [Performance](#performance)
27
+ - [FAQ](#faq)
28
+ - [Contributing](#contributing)
29
+ - [License](#license)
30
+
31
+ ## Features
32
+
33
+ - **Two schema languages, one engine**: a small DSL (`String()`, `Integer({ min: 18 })`...) and JSON Schema draft-07
34
+ compile to the same types and the same generated code.
35
+ - **Complete draft-07**: passes the whole [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite)
36
+ for draft-07 (929 of 929 tests), including `$ref` with JSON pointers, `$id` base URIs, anchors, recursive schemas
37
+ and references to other documents.
38
+ - **Readable errors** with the path of each field: `lines[12].price must be a number`.
39
+ - **Three modes**: every error, the first error only, or just `true`/`false`.
40
+ - **Fast**: faster than ajv in every [benchmark](#performance) we run, and about 50 times faster at compiling.
41
+ - **Strict**: unknown or unsupported keywords throw when compiling instead of being silently ignored.
42
+ - **No dependencies**, CommonJS and ESM, Node.js 18 and later, and browsers through any bundler.
43
+ - **Extensible**: classes of your own with a `validate()` method work inside compiled schemas.
44
+
45
+ ## Install
12
46
 
13
47
  ```sh
14
48
  npm install schiva
15
49
  ```
16
50
 
17
- ## Quick start
51
+ ## Getting started
52
+
53
+ With the schema DSL:
18
54
 
19
55
  ```js
20
56
  const { ClosedSchema, String, Integer, ArrayOf } = require('schiva');
@@ -36,7 +72,7 @@ validatePerson({ id: 1, age: 10, extra: 1 });
36
72
  With JSON Schema:
37
73
 
38
74
  ```js
39
- const { compileJsonSchema } = require('schiva');
75
+ import { compileJsonSchema } from 'schiva';
40
76
 
41
77
  const validate = compileJsonSchema({
42
78
  type: 'object',
@@ -48,6 +84,8 @@ validate({ id: 'AB' }); // []
48
84
  validate({ id: 'ab' }); // ['id does not match the required pattern']
49
85
  ```
50
86
 
87
+ `require('schiva')` and `import { ... } from 'schiva'` export the same names.
88
+
51
89
  ## Modes
52
90
 
53
91
  `compile(options)` (on schemas and on every type) and `compileJsonSchema(json, options)` return a function:
@@ -58,6 +96,14 @@ validate({ id: 'ab' }); // ['id does not match the required pattern']
58
96
  | `{ allErrors: false }` | only the first error message, `[]` when valid | one message is enough; stops at the first failing check |
59
97
  | `{ errors: false }` | `true` or `false` | only validity matters; builds no messages |
60
98
 
99
+ ```js
100
+ const isPerson = person.compile({ errors: false });
101
+
102
+ if (!isPerson(body)) {
103
+ // reject the request
104
+ }
105
+ ```
106
+
61
107
  Schemas and types also have `validate(value)`, which checks without compiling (10 to 25 times slower), and
62
108
  `isValid(value)`.
63
109
 
@@ -68,27 +114,43 @@ The compiled function is a snapshot of the schema: changes made to the schema or
68
114
  again if it changes. Compiling takes about 0.1 ms for a schema of moderate size, so compile when the program starts
69
115
  (or cache the function), not for every value.
70
116
 
117
+ ```js
118
+ // Good: compiled once, reused for every request.
119
+ const validateOrder = orderSchema.compile();
120
+ app.post('/orders', (req, res) => {
121
+ const errors = validateOrder(req.body);
122
+ if (errors.length) return res.status(400).json({ errors });
123
+ // ...
124
+ });
125
+ ```
126
+
71
127
  The generated code uses `new Function`, so it does not run where code generation is forbidden (for example with a
72
- strict Content Security Policy).
128
+ strict Content Security Policy). See [Security considerations](#security-considerations).
73
129
 
74
130
  ## Schema DSL
75
131
 
76
132
  A `Schema` takes an object whose keys are the expected properties. Plain objects inside it become nested schemas.
77
133
 
78
134
  ```js
79
- const { Schema, String, Float, Enum } = require('schiva');
135
+ const { Schema, String, Float, Enum, ArrayOf, Integer } = require('schiva');
80
136
 
81
137
  const order = new Schema(
82
138
  {
83
139
  id: String({ pattern: /^ORD-\d{8}$/ }),
84
140
  status: Enum({ options: ['draft', 'placed'] }),
85
141
  customer: { name: String({ min: 1 }), email: String({ isMandatory: false }) },
142
+ lines: ArrayOf({ type: { sku: String(), qty: Integer({ min: 1 }) }, min: 1 }),
86
143
  total: Float({ min: 0 }),
87
144
  },
88
145
  { isOpen: false }
89
146
  );
90
147
  ```
91
148
 
149
+ Wherever a type is expected (`ArrayOf`, `AnyOf`, `Not`, `additionalType`...), a plain object stands for
150
+ `new Schema(object)`, as `lines` shows above. That schema is open even inside a `ClosedSchema`; write
151
+ `new ClosedSchema({ ... })` to reject unknown keys there too. A value that is neither a type nor an object of types
152
+ throws when the type is built.
153
+
92
154
  Every type takes `isMandatory` (default `true`: `undefined` is an error) and `isNullable` (default `false`: `null` is
93
155
  an error), and has `.optional()`, `.required()`, `.nullable()` and `.notNull()`.
94
156
 
@@ -111,11 +173,27 @@ Schema options: `isOpen` (default `true`; `ClosedSchema` sets it to `false` and
111
173
  `dependencies` (`[{ key, required: [...] }]` or `[{ key, type }]`), `isMandatory`, `isNullable`.
112
174
 
113
175
  Short helpers are also exported: `str`, `int`, `float`, `bool`, `arrOf`, `obj`, `any`, `anyOf`, `allOf`, `oneOf`,
114
- `not`, `enumt`, and optional versions prefixed with `o` (`ostr`, `oint`...).
176
+ `not`, `enumt`, and optional versions prefixed with `o` (`ostr`, `oint`...). `arrOf(String())` and
177
+ `arrOf({ sku: String() })` take the type of the elements; `arrOf({ type, min, ... })` takes the options.
115
178
 
116
179
  Declared keys are read as own properties only, so `{}.toString` or a key added to `Object.prototype` never counts as
117
180
  present.
118
181
 
182
+ ### Recursive schemas
183
+
184
+ Create the `Ref` first and point it at the schema once the schema exists:
185
+
186
+ ```js
187
+ const { Schema, Integer, ArrayOf, Ref } = require('schiva');
188
+
189
+ const child = Ref();
190
+ const node = new Schema({ value: Integer(), children: ArrayOf({ type: child, isMandatory: false }) });
191
+ child.target = node;
192
+
193
+ node.compile()({ value: 1, children: [{ value: 2, children: [{ value: 'x' }] }] });
194
+ // ['children[0].children[0].value must be a number']
195
+ ```
196
+
119
197
  ## JSON Schema
120
198
 
121
199
  `fromJsonSchema(json, options)` converts a JSON Schema (draft-07) into the same types, and
@@ -123,6 +201,19 @@ present.
123
201
  `definitions` and `$ref` (JSON pointers, `$id` base URIs and anchors, recursive schemas). String lengths count Unicode
124
202
  code points, as the specification says.
125
203
 
204
+ A keyword schiva does not know throws when compiling, with the place where it was found:
205
+
206
+ ```js
207
+ compileJsonSchema({ type: 'string', maxLenght: 10 });
208
+ // Error: Unsupported JSON Schema keyword "maxLenght" at #
209
+ ```
210
+
211
+ Annotations (`title`, `description`, `default`, `examples`, `$comment`, `readOnly`, `writeOnly`, `deprecated`) are
212
+ accepted and ignored. So is **`format`**: draft-07 makes checking it optional, and schiva does not check it. Use
213
+ `pattern`, or a [type of your own](#types-of-your-own), for values such as emails or dates.
214
+
215
+ ### Several documents
216
+
126
217
  References to other documents resolve against the documents given in `options.schemas`, as `{ uri: schema }` or as an
127
218
  array of schemas with `$id`. Nothing is loaded from the network, and a reference that cannot be resolved throws when
128
219
  compiling:
@@ -136,16 +227,68 @@ const validateOrder = compileJsonSchema(orderSchema, {
136
227
  To validate schemas against the draft-07 meta-schema, register it in `schemas` (ajv ships a copy as
137
228
  `ajv/dist/refs/json-schema-draft-07.json`).
138
229
 
230
+ ## Errors
231
+
232
+ Errors are plain strings that start with the path of the field, so they can be logged or returned as they are:
233
+
234
+ ```js
235
+ [
236
+ 'customer.name is mandatory',
237
+ 'lines[1].sku must be a string',
238
+ 'lines[1].price must be at least 0',
239
+ 'Unexpected key: extra',
240
+ ];
241
+ ```
242
+
243
+ A value checked on its own (not inside a schema) is called `Value`: `Integer().compile()('x')` returns
244
+ `['Value must be a number']`.
245
+
139
246
  ## Types of your own
140
247
 
141
248
  Classes that extend `ValidateType` (or a built-in type) with their own `validate()` or `isValid()` keep working when
142
- compiled: the generated code calls them for their part of the schema.
249
+ compiled: the generated code calls them for their part of the schema. `validate()` returns `undefined` when the value
250
+ is valid and an error message otherwise.
251
+
252
+ ```js
253
+ const { Schema, ValidateType } = require('schiva');
254
+
255
+ class Even extends ValidateType {
256
+ validate(value, fieldName = 'Value') {
257
+ const presence = super.validate(value, fieldName); // isMandatory and isNullable
258
+ if (presence !== undefined || value === undefined || value === null) return presence;
259
+ return value % 2 === 0 ? undefined : `${fieldName} must be even`;
260
+ }
261
+ }
262
+
263
+ new Schema({ n: new Even() }).compile()({ n: 3 }); // ['n must be even']
264
+ ```
143
265
 
144
- ## Speed
266
+ ## Security considerations
267
+
268
+ - **Schemas are code.** Compiling turns a schema into JavaScript, so treat schemas like the code of your program:
269
+ compile schemas you wrote or trust, not schemas sent by users. A large or deeply nested schema is slow to compile
270
+ and to validate.
271
+ - **Regular expressions.** `pattern`, `patternProperties` and `String({ pattern })` run the regular expression you
272
+ give them on the data. A badly written one can take exponential time on some inputs
273
+ ([ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)). Keep them simple,
274
+ and limit the length of strings (`maxLength`, `String({ max })`) before matching long input.
275
+ - **Every error or the first one.** Collecting every error keeps checking after the first failure, so an invalid
276
+ value costs more. For untrusted input where one message is enough, use `{ allErrors: false }` or `{ errors: false }`.
277
+ - **Large inputs.** `uniqueItems`/`unique` compares items with each other, so limit the size of arrays
278
+ (`maxItems`, `ArrayOf({ max })`) that come from outside.
279
+ - **Content Security Policy.** The generated code is created with `new Function`, which needs `'unsafe-eval'` in the
280
+ `script-src` of a Content Security Policy. Where that is not allowed, use `validate()` and `isValid()`, which do not
281
+ generate code.
282
+ - **Circular data.** Values that contain themselves (`a.self = a`) are not supported.
283
+
284
+ Report security problems privately through [GitHub security advisories](https://github.com/jesus-seijas-sp/schiva/security/advisories/new),
285
+ not in public issues.
286
+
287
+ ## Performance
145
288
 
146
289
  Compared with ajv and the other validators of
147
290
  [json-schema-benchmark](https://github.com/ebdrup/json-schema-benchmark), each measurement in its own process
148
- (see `bench/`):
291
+ (see [`bench/`](bench)):
149
292
 
150
293
  | | schiva | ajv | @exodus/schemasafe |
151
294
  |---|---|---|---|
@@ -161,6 +304,32 @@ pnpm run bench # every validator in its own process (about 8 minutes)
161
304
  pnpm run bench:quick # every validator in one process (about a minute)
162
305
  ```
163
306
 
307
+ ## FAQ
308
+
309
+ **Should I use the DSL or JSON Schema?** Use JSON Schema when the schema is shared with other languages or tools
310
+ (OpenAPI, forms, other services). Use the DSL when the schema lives in your JavaScript code: it is shorter, and
311
+ `RegExp` patterns and types of your own fit in directly. Both compile to the same code, so the speed is the same.
312
+
313
+ **Why are errors strings and not objects?** Most errors end up in a log or an HTTP response. Strings with the path in
314
+ front are ready for that, and building them is cheap. If you only need to know whether a value is valid, use
315
+ `{ errors: false }`.
316
+
317
+ **Does it support draft 2019-09 or 2020-12?** Not yet: schiva implements draft-07. Keywords from later drafts, such as
318
+ `unevaluatedProperties` or `$defs`, throw when compiling.
319
+
320
+ **Does it check `format`?** No, see [JSON Schema](#json-schema).
321
+
322
+ **Does it include TypeScript types?** Not yet.
323
+
324
+ ## Contributing
325
+
326
+ Issues and pull requests are welcome at [github.com/jesus-seijas-sp/schiva](https://github.com/jesus-seijas-sp/schiva).
327
+
328
+ ```sh
329
+ pnpm install
330
+ pnpm test # ESLint and Jest with coverage
331
+ ```
332
+
164
333
  ## License
165
334
 
166
- MIT
335
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "schiva",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Fast schema validation for JavaScript: a schema DSL and JSON Schema (draft-07), compiled to generated code",
5
+ "homepage": "https://schiva.js.org",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jesus-seijas-sp/schiva.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/jesus-seijas-sp/schiva/issues"
12
+ },
5
13
  "main": "./src/index.js",
6
14
  "type": "commonjs",
7
15
  "files": [
@@ -14,7 +22,11 @@
14
22
  "json-schema",
15
23
  "draft-07",
16
24
  "compiler",
17
- "fast"
25
+ "fast",
26
+ "jsonschema",
27
+ "schema-validation",
28
+ "dsl",
29
+ "ajv-alternative"
18
30
  ],
19
31
  "author": "Jesús Seijas <jseijas@gmail.com>",
20
32
  "license": "MIT",
package/src/index.js CHANGED
@@ -1,13 +1,151 @@
1
- const closedSchema = require('./closed-schema');
2
- const compile = require('./compile');
3
- const jsonSchema = require('./json-schema');
4
- const schema = require('./schema');
5
- const types = require('./types');
1
+ const { ClosedSchema } = require('./closed-schema');
2
+ const { compileErrors, compileFirstError, compileIsValid, compileType } = require('./compile');
3
+ const { fromJsonSchema, compileJsonSchema } = require('./json-schema');
4
+ const { Schema } = require('./schema');
5
+ const {
6
+ AllOfType,
7
+ AllOf,
8
+ allOf,
9
+ oallOf,
10
+ AnyType,
11
+ Any,
12
+ any,
13
+ oany,
14
+ AnyOfType,
15
+ AnyOf,
16
+ anyOf,
17
+ oanyOf,
18
+ ArrayOfType,
19
+ ArrayOf,
20
+ arrOf,
21
+ oarrOf,
22
+ BooleanType,
23
+ Boolean,
24
+ bool,
25
+ obool,
26
+ ConditionalType,
27
+ Conditional,
28
+ EnumType,
29
+ Enum,
30
+ enumt,
31
+ oenumt,
32
+ oenum,
33
+ FloatType,
34
+ Float,
35
+ float,
36
+ ofloat,
37
+ num,
38
+ onum,
39
+ IntegerType,
40
+ Integer,
41
+ int,
42
+ oint,
43
+ NeverType,
44
+ Never,
45
+ never,
46
+ NotType,
47
+ Not,
48
+ not,
49
+ onot,
50
+ ObjType,
51
+ Obj,
52
+ obj,
53
+ oobj,
54
+ OneOfType,
55
+ OneOf,
56
+ oneOf,
57
+ ooneOf,
58
+ RefType,
59
+ Ref,
60
+ StringType,
61
+ String,
62
+ str,
63
+ ostr,
64
+ ValidateType,
65
+ hasErrors,
66
+ toErrors,
67
+ ValuesType,
68
+ Values,
69
+ Const,
70
+ WhenType,
71
+ When,
72
+ isJsonType,
73
+ } = require('./types');
6
74
 
7
75
  module.exports = {
8
- ...closedSchema,
9
- ...compile,
10
- ...jsonSchema,
11
- ...schema,
12
- ...types,
76
+ ClosedSchema,
77
+ compileErrors,
78
+ compileFirstError,
79
+ compileIsValid,
80
+ compileType,
81
+ fromJsonSchema,
82
+ compileJsonSchema,
83
+ Schema,
84
+ AllOfType,
85
+ AllOf,
86
+ allOf,
87
+ oallOf,
88
+ AnyType,
89
+ Any,
90
+ any,
91
+ oany,
92
+ AnyOfType,
93
+ AnyOf,
94
+ anyOf,
95
+ oanyOf,
96
+ ArrayOfType,
97
+ ArrayOf,
98
+ arrOf,
99
+ oarrOf,
100
+ BooleanType,
101
+ Boolean,
102
+ bool,
103
+ obool,
104
+ ConditionalType,
105
+ Conditional,
106
+ EnumType,
107
+ Enum,
108
+ enumt,
109
+ oenumt,
110
+ oenum,
111
+ FloatType,
112
+ Float,
113
+ float,
114
+ ofloat,
115
+ num,
116
+ onum,
117
+ IntegerType,
118
+ Integer,
119
+ int,
120
+ oint,
121
+ NeverType,
122
+ Never,
123
+ never,
124
+ NotType,
125
+ Not,
126
+ not,
127
+ onot,
128
+ ObjType,
129
+ Obj,
130
+ obj,
131
+ oobj,
132
+ OneOfType,
133
+ OneOf,
134
+ oneOf,
135
+ ooneOf,
136
+ RefType,
137
+ Ref,
138
+ StringType,
139
+ String,
140
+ str,
141
+ ostr,
142
+ ValidateType,
143
+ hasErrors,
144
+ toErrors,
145
+ ValuesType,
146
+ Values,
147
+ Const,
148
+ WhenType,
149
+ When,
150
+ isJsonType,
13
151
  };
package/src/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- const { ObjType, ValidateType } = require('./types');
1
+ const { ObjType, ValidateType, toType } = require('./types');
2
2
 
3
3
  // Declared keys are read as own properties only: {}.toString or {}.constructor must not count as present.
4
4
  // A value read from a plain object is its own unless Object.prototype has the key, which avoids the slower
@@ -22,16 +22,21 @@ class Schema {
22
22
  this.isMandatory = options.isMandatory === undefined ? true : options.isMandatory;
23
23
  this.isNullable = options.isNullable === undefined ? false : options.isNullable;
24
24
  // Type that keys not declared in the schema must satisfy (only used when the schema is open).
25
- this.additionalType = options.additionalType;
25
+ this.additionalType = toType(options.additionalType, 'Schema additionalType');
26
26
  // [{ pattern, type }]: keys matching a pattern must satisfy its type, and are not checked by additionalType.
27
- this.patternTypes = options.patternTypes || [];
27
+ this.patternTypes = (options.patternTypes || []).map((item, i) => ({
28
+ ...item,
29
+ type: toType(item.type, `Schema patternTypes[${i}].type`),
30
+ }));
28
31
  this.minProperties = options.minProperties;
29
32
  this.maxProperties = options.maxProperties;
30
33
  // [{ key, required: [properties] } or { key, type }]: when key is present, the properties must be present too,
31
34
  // or the whole object must satisfy type.
32
- this.dependencies = options.dependencies || [];
35
+ this.dependencies = (options.dependencies || []).map((item, i) =>
36
+ item.type === undefined ? item : { ...item, type: toType(item.type, `Schema dependencies[${i}].type`) }
37
+ );
33
38
  // Type every key must satisfy, reported as "Key <name>".
34
- this.propertyNameType = options.propertyNameType;
39
+ this.propertyNameType = toType(options.propertyNameType, 'Schema propertyNameType');
35
40
  this.visitObjs();
36
41
  this.keys = Object.keys(this.schema);
37
42
  this.keySet = new Set(this.keys);
@@ -1,10 +1,10 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toTypes } = require('./validate-type');
2
2
 
3
3
  // Value must satisfy every type; reports the errors of the first type that fails.
4
4
  class AllOfType extends ValidateType {
5
5
  constructor(options = {}) {
6
6
  super(options);
7
- this.types = options.types || [];
7
+ this.types = toTypes(options.types, 'AllOf types') || [];
8
8
  }
9
9
 
10
10
  validate(value, fieldName = 'Value') {
@@ -1,9 +1,9 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toTypes } = require('./validate-type');
2
2
 
3
3
  class AnyOfType extends ValidateType {
4
4
  constructor(options = {}) {
5
5
  super(options);
6
- this.types = options.types;
6
+ this.types = toTypes(options.types, 'AnyOf types');
7
7
  }
8
8
 
9
9
  validate(value, fieldName = 'Value') {
@@ -1,18 +1,20 @@
1
1
  const { hasDuplicates } = require('./has-duplicates');
2
- const { ValidateType } = require('./validate-type');
2
+ const { ValidateType, isPlainObject, toType, toTypes } = require('./validate-type');
3
3
 
4
4
  class ArrayOfType extends ValidateType {
5
5
  constructor(options = {}) {
6
6
  super(options);
7
7
  // A type for every element, or an array of types for the elements at each position (a tuple).
8
- this.type = options.type;
8
+ this.type = Array.isArray(options.type)
9
+ ? toTypes(options.type, 'ArrayOf type')
10
+ : toType(options.type, 'ArrayOf type');
9
11
  this.min = options.min;
10
12
  this.max = options.max;
11
13
  this.unique = options.unique;
12
14
  // At least one element must satisfy it.
13
- this.contains = options.contains;
15
+ this.contains = toType(options.contains, 'ArrayOf contains');
14
16
  // With a tuple, the elements after its last position must satisfy it.
15
- this.additionalType = options.additionalType;
17
+ this.additionalType = toType(options.additionalType, 'ArrayOf additionalType');
16
18
  }
17
19
 
18
20
  hasMatch(value) {
@@ -115,15 +117,27 @@ function ArrayOf(options) {
115
117
  return new ArrayOfType(options);
116
118
  }
117
119
 
120
+ const OPTION_KEYS = ['type', 'min', 'max', 'unique', 'contains', 'additionalType', 'isMandatory', 'isNullable'];
121
+
122
+ // The first argument of arrOf() is the options when it is a plain object that is empty or has an option key;
123
+ // otherwise it is the type of the elements (a type, a schema, or a plain object of types).
124
+ function isOptions(value) {
125
+ if (!isPlainObject(value)) {
126
+ return false;
127
+ }
128
+ const keys = Object.keys(value);
129
+ return keys.length === 0 || keys.some((key) => OPTION_KEYS.includes(key));
130
+ }
131
+
118
132
  function arrOf(type, min, max, isMandatory = true, isNullable = false) {
119
- if (type !== undefined && type !== null && typeof type === 'object') {
133
+ if (isOptions(type)) {
120
134
  return new ArrayOfType(type);
121
135
  }
122
136
  return new ArrayOfType({ type, min, max, isMandatory, isNullable });
123
137
  }
124
138
 
125
139
  function oarrOf(type, min, max, isMandatory = false, isNullable = false) {
126
- if (type !== undefined && type !== null && typeof type === 'object') {
140
+ if (isOptions(type)) {
127
141
  return new ArrayOfType({ isMandatory: false, ...type });
128
142
  }
129
143
  return new ArrayOfType({ type, min, max, isMandatory, isNullable });
@@ -1,13 +1,13 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toType } = require('./validate-type');
2
2
 
3
3
  // When the value satisfies `ifType` it must satisfy `thenType`, otherwise `elseType`; a missing branch accepts
4
4
  // anything. Only the errors of the branch are reported, like JSON Schema if/then/else.
5
5
  class ConditionalType extends ValidateType {
6
6
  constructor(options = {}) {
7
7
  super(options);
8
- this.ifType = options.ifType;
9
- this.thenType = options.thenType;
10
- this.elseType = options.elseType;
8
+ this.ifType = toType(options.ifType, 'Conditional ifType');
9
+ this.thenType = toType(options.thenType, 'Conditional thenType');
10
+ this.elseType = toType(options.elseType, 'Conditional elseType');
11
11
  }
12
12
 
13
13
  branch(value) {
package/src/types/not.js CHANGED
@@ -1,10 +1,10 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toType } = require('./validate-type');
2
2
 
3
3
  // Value must not satisfy `type`.
4
4
  class NotType extends ValidateType {
5
5
  constructor(options = {}) {
6
6
  super(options);
7
- this.type = options.type;
7
+ this.type = toType(options.type, 'Not type');
8
8
  }
9
9
 
10
10
  validate(value, fieldName = 'Value') {
@@ -1,10 +1,10 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toTypes } = require('./validate-type');
2
2
 
3
3
  // Value must satisfy exactly one of the types. When none does, reports the errors of every type, like AnyOfType.
4
4
  class OneOfType extends ValidateType {
5
5
  constructor(options = {}) {
6
6
  super(options);
7
- this.types = options.types || [];
7
+ this.types = toTypes(options.types, 'OneOf types') || [];
8
8
  }
9
9
 
10
10
  // Number of types the value satisfies, counting up to 2.
package/src/types/ref.js CHANGED
@@ -1,4 +1,4 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toType } = require('./validate-type');
2
2
 
3
3
  // Validates with the type it refers to, which is set once references are resolved; recursive schemas refer back to a
4
4
  // type that contains the reference. Only undefined is handled here (isMandatory); null and other values go to the
@@ -7,7 +7,7 @@ class RefType extends ValidateType {
7
7
  constructor(options = {}) {
8
8
  super(options);
9
9
  this.ref = options.ref;
10
- this.target = options.target;
10
+ this.target = toType(options.target, 'Ref target');
11
11
  }
12
12
 
13
13
  getTarget() {
@@ -91,8 +91,46 @@ function toErrors(result) {
91
91
  return result ? [result] : [];
92
92
  }
93
93
 
94
+ function isPlainObject(value) {
95
+ if (value === null || typeof value !== 'object') {
96
+ return false;
97
+ }
98
+ const proto = Object.getPrototypeOf(value);
99
+ return proto === Object.prototype || proto === null;
100
+ }
101
+
102
+ // Normalizes an option that holds a type. A plain object stands for new Schema(object), as it does for a key of a
103
+ // Schema; other objects (types, schemas) are kept; anything else throws now instead of failing when validating.
104
+ function toType(value, name) {
105
+ if (value === undefined || value instanceof ValidateType) {
106
+ return value;
107
+ }
108
+ if (isPlainObject(value)) {
109
+ // eslint-disable-next-line global-require -- schema.js requires this module
110
+ const { Schema } = require('../schema');
111
+ return new Schema(value);
112
+ }
113
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
114
+ throw new TypeError(`${name} must be a type or an object of types`);
115
+ }
116
+ return value;
117
+ }
118
+
119
+ function toTypes(values, name) {
120
+ if (values === undefined) {
121
+ return values;
122
+ }
123
+ if (!Array.isArray(values)) {
124
+ throw new TypeError(`${name} must be an array of types`);
125
+ }
126
+ return values.map((value, i) => toType(value, `${name}[${i}]`));
127
+ }
128
+
94
129
  module.exports = {
95
130
  ValidateType,
96
131
  hasErrors,
97
132
  toErrors,
133
+ isPlainObject,
134
+ toType,
135
+ toTypes,
98
136
  };
package/src/types/when.js CHANGED
@@ -1,4 +1,4 @@
1
- const { ValidateType } = require('./validate-type');
1
+ const { ValidateType, toType } = require('./validate-type');
2
2
 
3
3
  const JSON_TYPES = ['object', 'array', 'string', 'number'];
4
4
 
@@ -25,7 +25,7 @@ class WhenType extends ValidateType {
25
25
  throw new Error(`WhenType jsonType must be one of: ${JSON_TYPES.join(', ')}`);
26
26
  }
27
27
  this.jsonType = options.jsonType;
28
- this.type = options.type;
28
+ this.type = toType(options.type, 'When type');
29
29
  }
30
30
 
31
31
  validate(value, fieldName) {