isvalid 2.10.3 → 3.0.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/.eslintrc.js CHANGED
@@ -5,7 +5,7 @@ module.exports = {
5
5
  'mocha': true
6
6
  },
7
7
  'parserOptions': {
8
- 'ecmaVersion': 2017
8
+ 'ecmaVersion': 2018
9
9
  },
10
10
  'extends': 'eslint:recommended',
11
11
  'rules': {
package/README.md CHANGED
@@ -42,6 +42,7 @@
42
42
  * [`enum`](#enum)
43
43
  - [`Number` Validators](#number-validators)
44
44
  * [`range`](#range)
45
+ * [`float`](#float)
45
46
  - [Custom Types](#custom-types)
46
47
  * [`post`](#post)
47
48
  + [Example](#example-1)
@@ -448,6 +449,26 @@ Examples:
448
449
  }
449
450
  ````
450
451
 
452
+ > Negative values can be wrapped in parentheses.
453
+
454
+ ````javascript
455
+ {
456
+ type: Array,
457
+ len: '(-2)-2',
458
+ schema: { … }
459
+ }
460
+ ````
461
+
462
+ > It also supports non-integer values.
463
+
464
+ ````javascript
465
+ {
466
+ type: Array,
467
+ len: '(-2.2)-2.2',
468
+ schema: { … }
469
+ }
470
+ ````
471
+
451
472
  > An array that should have at least 2 items, exactly 5 items or 8 or more items.
452
473
 
453
474
  ##### `unique`
@@ -498,7 +519,7 @@ This does not do any actual validation. Instead it trims the input in both ends
498
519
  ##### `len`
499
520
  Type: `String` or `Number`
500
521
 
501
- This ensures that the string's length is within a specified range. You can use the same formatting as [`Array`'s `len`](#len) validator.
522
+ This ensures that the string's length is within a specified range. You can use the same formatting as [`Array`'s `len`](#len) validator described above (except it does not support ranges with negative values or non-integers).
502
523
 
503
524
  ##### `match`
504
525
  Type: `RegExp`
@@ -535,7 +556,21 @@ Type: `Number`or `String`
535
556
 
536
557
  This ensures that the number is within a certain range. If not the validator throws an error.
537
558
 
538
- The `range` validator uses the same formatting as the [`Array`'s `len`](#len) validator described above.
559
+ The `range` validator uses the same formatting as the [`Array`'s `len`](#len) validator described above (except it does not support ranges with negative values or non-integers).
560
+
561
+ ##### `float`
562
+ Type: `String` of value: `'allow'`, `'deny'`, `'round'`, `'floor'`, `'ceil'`
563
+
564
+ This tells the validator how to handle non-integers.
565
+
566
+ The validator has five options:
567
+ * `'allow'` Allow non-integer values.
568
+ * `'deny'` Throw a `ValidationError` if the value is a non-integer.
569
+ * `'round'` Round value to nearest integer.
570
+ * `'floor'` Round to integer less than or equal to value.
571
+ * `'ceil'` Round to integer bigger than or equal to value.
572
+
573
+ > Default is `'allow'`.
539
574
 
540
575
  #### Custom Types
541
576
 
package/lib/formalize.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  const merge = require('merge'),
4
4
  SchemaError = require('./errors/schema.js'),
5
- utils = require('./utils.js');
5
+ utils = require('./utils.js'),
6
+ ranges = require('./ranges.js');
6
7
 
7
8
  const finalize = (formalizedSchema, nonFormalizedSchema) => {
8
9
 
@@ -77,7 +78,7 @@ const formalizeAny = (schema, options = {}) => {
77
78
  return formalizeObject({ type: Object, schema: schema }, schema, options);
78
79
  }
79
80
  if ('array' == utils.instanceTypeName(schema)) {
80
- if (schema.length === 0) throw new SchemaError(schema, 'Array must have exactly one schema.');
81
+ if (schema.length === 0) return formalizeAny({ type: Array }, schema. options);
81
82
  return formalizeArray({ type: Array, schema: schema[0] }, schema, options);
82
83
  }
83
84
  if ((typeof schema === 'string' && schema.length) || (typeof schema === 'function' && utils.typeName(schema) !== undefined)) {
@@ -125,7 +126,8 @@ const formalizeAny = (schema, options = {}) => {
125
126
  'enum': [ 'array' ]
126
127
  });
127
128
  if (utils.isSameType('number', utils.typeName(type))) merge(validators, {
128
- 'range': [ 'string', 'number' ]
129
+ 'range': [ 'string', 'number' ],
130
+ 'float': [ 'string' ]
129
131
  });
130
132
  }
131
133
 
@@ -209,6 +211,31 @@ const formalizeAny = (schema, options = {}) => {
209
211
  );
210
212
  }
211
213
 
214
+ // Check number float
215
+ if (typeof formalizedSchema.float === 'string' &&
216
+ ['allow', 'deny', 'round', 'floor', 'ceil'].indexOf(formalizedSchema.float) == -1) {
217
+ throw new SchemaError(
218
+ schema,
219
+ 'Validator `float` must have value `\'allow\'`, `\'deny\'`, `\'round\'`, `\'floor\'` or `\'ceil\'`.'
220
+ );
221
+ }
222
+
223
+ // Check len
224
+ if (typeof formalizedSchema.len !== 'undefined') {
225
+ formalizedSchema.len = ranges.formalize(formalizedSchema.len, {
226
+ allowNegative: false,
227
+ allowNonIntegers: false
228
+ });
229
+ }
230
+
231
+ // Check range
232
+ if (typeof formalizedSchema.range !== 'undefined') {
233
+ formalizedSchema.range = ranges.formalize(formalizedSchema.range, {
234
+ allowNegative: true,
235
+ allowNonIntegers: true
236
+ });
237
+ }
238
+
212
239
  // Check string enums
213
240
  if (typeof formalizedSchema.enum !== 'undefined') {
214
241
  if (formalizedSchema.enum.length < 1) {
package/lib/key-paths.js CHANGED
@@ -8,7 +8,17 @@ exports = module.exports = (schema, formalizeOptions) => {
8
8
  schema = formalize(schema, formalizeOptions);
9
9
 
10
10
  return {
11
- all: (types, keyPath = []) => {
11
+ all: (types, options, keyPath = [], level = 0) => {
12
+
13
+ if (typeof types === 'object' && types !== null && !Array.isArray(types)) {
14
+ options = types;
15
+ types = [];
16
+ }
17
+
18
+ options = options || {};
19
+ options.maxDepth = typeof options.maxDepth !== 'undefined' ? options.maxDepth : Infinity;
20
+
21
+ if (typeof options.maxDepth !== 'number') throw new Error('Maximum depth must be a number.');
12
22
 
13
23
  if (!types) types = [];
14
24
  if (!Array.isArray(types)) types = [types];
@@ -21,15 +31,19 @@ exports = module.exports = (schema, formalizeOptions) => {
21
31
 
22
32
  if (types.length == 0 || types.includes(typeName)) result.push(keyPath.join('.'));
23
33
 
24
- switch (typeName) {
25
- case 'object':
26
- result = result.concat(...Object.keys(schema.schema).map((key) => {
27
- return exports(schema.schema[key]).all(types, keyPath.concat([key]));
28
- }));
29
- break;
30
- case 'array':
31
- result = result.concat(exports(schema.schema).all(types, keyPath));
32
- break;
34
+ if (level < options.maxDepth) {
35
+
36
+ switch (typeName) {
37
+ case 'object':
38
+ result = result.concat(...Object.keys(schema.schema).map((key) => {
39
+ return exports(schema.schema[key]).all(types, options, keyPath.concat([key]), level + 1);
40
+ }));
41
+ break;
42
+ case 'array':
43
+ result = result.concat(exports(schema.schema).all(types, options, keyPath, level + 1));
44
+ break;
45
+ }
46
+
33
47
  }
34
48
 
35
49
  let uniqueResult = [];
package/lib/ranges.js CHANGED
@@ -1,6 +1,15 @@
1
1
  'use strict';
2
2
 
3
- module.exports.testIndex = function(ranges, index) {
3
+ const
4
+ SchemaError = require('./errors/schema.js');
5
+
6
+ module.exports.testIndex = function(ranges, value) {
7
+ return ranges.some(({ lower, upper }) => {
8
+ return lower <= value && upper >= value;
9
+ });
10
+ };
11
+
12
+ module.exports.formalize = function(ranges, options) {
4
13
 
5
14
  // Convert to string if ranges is a Number.
6
15
  if (ranges !== undefined && typeof ranges === 'number') {
@@ -12,45 +21,50 @@ module.exports.testIndex = function(ranges, index) {
12
21
  throw new Error('Ranges must be a number or a string expressed as: ex. \'-2,4-6,8,10-\'.');
13
22
  }
14
23
 
15
- // Throw if index is not a number.
16
- if (index === undefined || typeof index !== 'number') {
17
- throw new Error('Index is not a number.');
18
- }
24
+ return ranges
25
+ .split(',')
26
+ .map((range) => {
19
27
 
20
- // Split into individual ranges.
21
- let r = ranges.split(',');
22
- for (let idx in r) {
28
+ // Get the boundaries of the range.
29
+ let boundaries = range.split(/(?<!\()-/);
23
30
 
24
- // Get the boundaries of the range.
25
- let boundaries = r[idx].split('-');
31
+ // Low and high boundaries are the same where only one number is specified.
32
+ if (boundaries.length == 1) boundaries = [ boundaries[0], boundaries[0] ];
33
+ // Throw an error if there is not exactly to boundaries.
34
+ if (boundaries.length != 2) throw new SchemaError('Malformed range \'' + range + '\'.');
26
35
 
27
- // Low and high boundaries are the same where only one number is specified.
28
- if (boundaries.length == 1) boundaries = [ boundaries[0], boundaries[0] ];
29
- // Throw an error if there is not exactly to boundaries.
30
- if (boundaries.length != 2) throw new Error('Malformed range \'' + r[idx] + '\'.');
36
+ // Test for malformed boundaries
37
+ for (let bIdx = 0 ; bIdx < 2 ; bIdx++) {
38
+ if (!/^\(?[-0-9.]*\)?$/.test(boundaries[bIdx])) throw new SchemaError('Malformed boundary \'' + boundaries[bIdx] + '\'.');
39
+ }
31
40
 
32
- // Test for malformed boundaries
33
- for (let bIdx = 0 ; bIdx < 2 ; bIdx++) {
34
- if (!/^[0-9]*$/.test(boundaries[bIdx])) throw new Error('Malformed boundary \'' + boundaries[bIdx] + '\'.');
35
- }
41
+ boundaries = boundaries.map((boundary) => boundary.match(/^\(?([-0-9.]*)\)?$/)[1] || '');
36
42
 
37
- let lowBoundary = boundaries[0];
38
- let highBoundary = boundaries[1];
43
+ let lower = boundaries[0];
44
+ let upper = boundaries[1];
39
45
 
40
- // If no lower boundary is specified we use -Infinity
41
- if (lowBoundary.length === 0) lowBoundary = -Infinity;
42
- else lowBoundary = parseInt(lowBoundary);
46
+ if ((options || {}).allowNegative === false && (lower < 0 || upper < 0)) {
47
+ throw new SchemaError('Boundary cannot be a negative value.');
48
+ }
43
49
 
44
- // If no higher boundary is specified we use Infinity;
45
- if (highBoundary.length === 0) highBoundary = Infinity;
46
- else highBoundary = parseInt(highBoundary);
50
+ if ((options || {}).allowNonInteger === false && (!Number.isInteger(lower) || !Number.isInteger(upper))) {
51
+ throw new SchemaError('Boundary cannot be a non-integer value.');
52
+ }
47
53
 
48
- // If index is within boundaries return true;
49
- if (index >= lowBoundary && index <= highBoundary) return true;
54
+ // If no lower boundary is specified we use -Infinity
55
+ if (lower.length === 0) lower = -Infinity;
56
+ else lower = parseFloat(lower);
50
57
 
51
- }
58
+ // If no higher boundary is specified we use Infinity;
59
+ if (upper.length === 0) upper = Infinity;
60
+ else upper = parseFloat(upper);
61
+
62
+ if (lower > upper) {
63
+ throw new SchemaError('Malformed range \'' + range +'\'');
64
+ }
65
+
66
+ return { lower, upper };
52
67
 
53
- // Index was not matched to any range.
54
- return false;
68
+ });
55
69
 
56
70
  };
package/lib/validate.js CHANGED
@@ -30,6 +30,9 @@ const validateObject = async (data, schema, options, keyPath, validatedData) =>
30
30
  );
31
31
  }
32
32
 
33
+ // If there is no schema we just return the object.
34
+ if (typeof schema.schema === 'undefined') return data;
35
+
33
36
  // Find unknown keys
34
37
  for (let key in data) {
35
38
  if (schema.schema[key] === undefined) {
@@ -87,9 +90,11 @@ const validateArray = async (data, schema, options, keyPath, validatedData) => {
87
90
  }
88
91
  }
89
92
 
90
- data = await Promise.all(data.map((data, idx) => {
91
- return validateAny(data, schema.schema, options, keyPath.concat([idx]), validatedData);
92
- }));
93
+ if (typeof schema.schema !== 'undefined') {
94
+ data = await Promise.all(data.map((data, idx) => {
95
+ return validateAny(data, schema.schema, options, keyPath.concat([idx]), validatedData);
96
+ }));
97
+ }
93
98
 
94
99
  if ((schema.len || options.defaults.len) && !ranges.testIndex((schema.len || options.defaults.len), data.length)) {
95
100
  throw new ValidationError(
@@ -201,6 +206,30 @@ const validateNumber = async (data, schema, options, keyPath) => {
201
206
  }
202
207
  }
203
208
 
209
+ if (schema.float || options.defaults.float) {
210
+ if (!Number.isInteger(data)) {
211
+ switch (schema.float) {
212
+ case 'deny':
213
+ throw new ValidationError(
214
+ keyPath,
215
+ schema._nonFormalizedSchema,
216
+ 'unknownKeys',
217
+ (schema.errors || {}).float || customErrorMessage(((options.errorMessages || {}).number || {}).float || 'Number must be an integer.'));
218
+ case 'round':
219
+ data = Math.round(data);
220
+ break;
221
+ case 'floor':
222
+ data = Math.floor(data);
223
+ break;
224
+ case 'ceil':
225
+ data = Math.ceil(data);
226
+ break;
227
+ default:
228
+ break;
229
+ }
230
+ }
231
+ }
232
+
204
233
  return data;
205
234
 
206
235
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isvalid",
3
- "version": "2.10.3",
3
+ "version": "3.0.0",
4
4
  "description": "Async JSON validation library for node.js.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -18,13 +18,13 @@
18
18
  "merge": "^2.1.1"
19
19
  },
20
20
  "devDependencies": {
21
- "body-parser": "^1.19.0",
22
- "chai": "^4.2.0",
21
+ "body-parser": "^1.20.0",
22
+ "chai": "^4.3.6",
23
23
  "chai-as-promised": "^7.1.1",
24
- "eslint": "^8.2.0",
25
- "express": "^4.17.1",
26
- "mocha": "^9.1.3",
27
- "supertest": "^6.1.6"
24
+ "eslint": "^8.12.0",
25
+ "express": "^4.17.3",
26
+ "mocha": "^9.2.2",
27
+ "supertest": "^6.2.2"
28
28
  },
29
29
  "scripts": {
30
30
  "test": "./node_modules/mocha/bin/mocha ./test/index.js"
package/test/formalize.js CHANGED
@@ -12,9 +12,6 @@ const f = (...args) => {
12
12
 
13
13
  describe('schema', function() {
14
14
  describe('formalizer', function() {
15
- it('should throw an error if array shortcut contains no object.', () => {
16
- expect(f([])).to.throw(SchemaError);
17
- });
18
15
  it('should throw an error if schema is garbage value.', () => {
19
16
  expect(f(123)).to.throw(SchemaError);
20
17
  });
@@ -168,10 +165,19 @@ describe('schema', function() {
168
165
  equal: '123'
169
166
  })).to.have.property('equal').equal('123');
170
167
  });
171
- it('should come back with a priority if non is provided', () => {
168
+ it('should come back with a priority if non is provided.', () => {
172
169
  expect(formalize({
173
170
  type: Number
174
171
  })).to.have.property('priority').equal(10);
175
172
  });
173
+ it ('should throw error if number range is not valid.', () => {
174
+ expect(f({ type: Number, range: 'abc-abc' })).to.throw(SchemaError);
175
+ });
176
+ it ('should throw error if string len is negative.', () => {
177
+ expect(f({ type: String, len: '-2-' })).to.throw(SchemaError);
178
+ });
179
+ it ('should throw error if array len is negative.', () => {
180
+ expect(f({ type: Array, len: '-2-' })).to.throw(SchemaError);
181
+ });
176
182
  });
177
183
  });
package/test/ranges.js CHANGED
@@ -3,31 +3,42 @@
3
3
  const expect = require('chai').expect,
4
4
  ranges = require('../lib/ranges.js');
5
5
 
6
+ function r(range, value) {
7
+ const formalized = ranges.formalize(range);
8
+ return ranges.testIndex(formalized, value);
9
+ }
10
+
6
11
  describe('ranges', function() {
7
12
  it ('should throw an error if ranges is not a string.', function() {
8
13
  expect(function() {
9
- ranges.testIndex([123], 1);
14
+ r([123], 1);
10
15
  }).to.throw(Error);
11
16
  });
12
17
  it ('should throw no error if ranges is a number.', function() {
13
18
  expect(function() {
14
- ranges.testIndex(1, 1);
19
+ r(1, 1);
15
20
  }).not.to.throw(Error);
16
21
  });
17
22
  it ('should throw error if ranges is string but format is invalid.', function() {
18
23
  expect(function() {
19
- ranges.testIndex('abc', 1);
24
+ r('abc', 1);
20
25
  }).to.throw(Error);
21
26
  });
22
27
  it ('should throw error if index is not set.', function() {
23
28
  expect(function() {
24
- ranges.testIndex(1);
29
+ r(1);
25
30
  });
26
31
  });
27
32
  it ('should return true if index is within range.', function() {
28
- expect(ranges.testIndex('-2,4-6,8,10-', 2)).to.equal(true);
33
+ expect(r('-2,4-6,8,10-', 2)).to.equal(true);
29
34
  });
30
35
  it ('should return false if index is not within range.', function() {
31
- expect(ranges.testIndex('-2,4-6,8,10-', 3)).to.equal(false);
36
+ expect(r('-2,4-6,8,10-', 3)).to.equal(false);
37
+ });
38
+ it ('should allow negative values wrapped in parentheses.', function() {
39
+ expect(r('(-2)-', -1)).to.equal(true);
40
+ });
41
+ it ('should allow for decimal values.', function() {
42
+ expect(r('(-2.2)-2.2', 0.1)).to.equal(true);
32
43
  });
33
44
  });
package/test/validate.js CHANGED
@@ -360,6 +360,9 @@ describe('validate', function() {
360
360
  });
361
361
  describe('object validator', function() {
362
362
  commonTests.all(Object, {}, 123);
363
+ it('should come back with same input if sub-schema is not provided.', () => {
364
+ return expect(isvalid({}, {})).to.eventually.eql({});
365
+ });
363
366
  it('should come out with same input as output if keys can validate.', () => {
364
367
  let s = isvalid({
365
368
  awesome: true,
@@ -480,7 +483,10 @@ describe('validate', function() {
480
483
  });
481
484
  describe('array validator', function() {
482
485
  commonTests.all(Array, [], 123);
483
- it('should come out with same input as output if array can validate.', () => {
486
+ it('should come back with same input if no sub-schema is provided.', () => {
487
+ return expect(isvalid([], [])).to.eventually.eql([]);
488
+ });
489
+ it('should come back with same input as output if array can validate.', () => {
484
490
  let s = isvalid([{
485
491
  awesome: true,
486
492
  why: 'it just is!'
@@ -706,6 +712,21 @@ describe('validate', function() {
706
712
  return expect(isvalid(123, Number))
707
713
  .to.eventually.equal(123);
708
714
  });
715
+ it('should throw error if non-integers are not allowed.', () => {
716
+ expect(isvalid(2.2, { type: Number, float: 'deny' })).to.eventually.throw(ValidationError);
717
+ });
718
+ it ('should come back with non-integer values if they are allowed.', () => {
719
+ expect(isvalid(2.2, { type: Number })).to.eventually.equal(2.2);
720
+ });
721
+ it ('should come back with number rounded if `float` is set to `round`.', () => {
722
+ expect(isvalid(2.5, { type: Number, float: 'round' })).to.eventually.equal(3);
723
+ });
724
+ it ('should come back with number ceiled if `float` is set to `ceil`.', () => {
725
+ expect(isvalid(2.2, { type: Number, float: 'ceil' })).to.eventually.equal(3);
726
+ });
727
+ it ('should come back with number floored if `float` is set to `floor`.', () => {
728
+ expect(isvalid(2.8, { type: Number, float: 'ceil' })).to.eventually.equal(2);
729
+ });
709
730
  describe('range', function() {
710
731
  it('should come back with error if input is not within range.', () => {
711
732
  return expect(isvalid(1, { type: Number, range: '2-4' }))