Split into separate methods, make literal string mean const
parent
fd140bfa31
commit
2122dc7288
|
@ -2,16 +2,16 @@
|
|||
* Yet another omnivorous data validator
|
||||
*
|
||||
* Peculiar features:
|
||||
* - Uses literal definitions (unlike, for example, Joi)
|
||||
* - Uses literal definitions (unlike Joi!)
|
||||
* - Simpler than JSON schema, but convertible to it
|
||||
* - Supports comments and convenient user-readable validation errors
|
||||
* - Converts strings to numbers if possible
|
||||
* - Converts scalars to single-element arrays
|
||||
* - Validates its own meta-type
|
||||
* - Validates itself (unlike JSON Schema and Joi too)
|
||||
*
|
||||
* (c) Vitaliy Filippov 2019+
|
||||
*
|
||||
* Version 2019-09-21
|
||||
* Version 2019-09-22
|
||||
*/
|
||||
const UserError = require('./UserError.js');
|
||||
|
||||
|
@ -24,65 +24,119 @@ class ServerPropTypes
|
|||
return_error: true,
|
||||
};
|
||||
type = this.fill(type, sub);
|
||||
let bad = false;
|
||||
switch (type.type)
|
||||
const fn = this['check_'+type.type];
|
||||
if (!fn)
|
||||
{
|
||||
case 'any':
|
||||
break;
|
||||
case 'string':
|
||||
throw new Error('unknown-type');
|
||||
}
|
||||
const r = fn.call(this, v, type, sub);
|
||||
if (r.error)
|
||||
{
|
||||
if (r.error === true)
|
||||
{
|
||||
r.error = {
|
||||
path: [],
|
||||
name: type.comment,
|
||||
};
|
||||
}
|
||||
if (options && options.varname)
|
||||
{
|
||||
r.error.name = options.varname + (r.error.name ? ' - ' + r.error.name : '');
|
||||
}
|
||||
if (options && options.return_error)
|
||||
{
|
||||
return { error: r.error };
|
||||
}
|
||||
throw new UserError('invalid-format', r.error);
|
||||
}
|
||||
return options && options.return_error ? { value: r.value } : r.value;
|
||||
}
|
||||
|
||||
static check_any(v, type)
|
||||
{
|
||||
return { value: v };
|
||||
}
|
||||
|
||||
static check_string(v, type)
|
||||
{
|
||||
let error = false;
|
||||
if (v != null && typeof v != 'number' && typeof v != 'string' || type.required && (v == null || v === ''))
|
||||
bad = true;
|
||||
error = true;
|
||||
else
|
||||
{
|
||||
v = ''+v;
|
||||
if (type.regex && !new RegExp(type.regex, type.regexOptions).exec(v))
|
||||
bad = true;
|
||||
error = true;
|
||||
}
|
||||
break;
|
||||
case 'decimal':
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_decimal(v, type)
|
||||
{
|
||||
let error = false;
|
||||
if (v != null && typeof v != 'number' && typeof v != 'string' || type.required && (v == null || v == 0))
|
||||
bad = true;
|
||||
error = true;
|
||||
else if (type.decimals != null)
|
||||
{
|
||||
const re = type.decimals > 0 ? /^-?\d+(\.\d+?)?0*$/ : /^-?\d+$/;
|
||||
let m = re.exec(''+v);
|
||||
if (m[1] && m[1].length > (type.decimals < 0 ? 0 : type.decimals) + 1)
|
||||
bad = true;
|
||||
error = true;
|
||||
}
|
||||
break;
|
||||
case 'num':
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_num(v, type)
|
||||
{
|
||||
let error = false;
|
||||
v = v == null ? null : parseFloat(v);
|
||||
if (v != v)
|
||||
bad = true;
|
||||
error = true;
|
||||
else if (type.min != null && v < type.min || type.max != null && v > type.max)
|
||||
bad = true;
|
||||
error = true;
|
||||
else if (type.required && !v)
|
||||
bad = true;
|
||||
break;
|
||||
case 'int':
|
||||
error = true;
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_int(v, type)
|
||||
{
|
||||
let error = false;
|
||||
v = v == null ? null : parseInt(v);
|
||||
if (v != v)
|
||||
bad = true;
|
||||
error = true;
|
||||
else if (type.min != null && v < type.min || type.max != null && v > type.max)
|
||||
bad = true;
|
||||
error = true;
|
||||
else if (type.required && !v)
|
||||
bad = true;
|
||||
break;
|
||||
case 'id':
|
||||
error = true;
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_id(v, type)
|
||||
{
|
||||
let error = false;
|
||||
v = v ? parseInt(v) : null;
|
||||
if (v != v || v < 0 || v == null && type.required)
|
||||
bad = true;
|
||||
break;
|
||||
case 'bool':
|
||||
error = true;
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_bool(v, type)
|
||||
{
|
||||
let error = false;
|
||||
if ((v != null || type.required) && v !== false && v !== true &&
|
||||
v !== '' && v !== 0 && v !== '0' && v !== 1 && v !== '1')
|
||||
bad = true;
|
||||
error = true;
|
||||
if (v != null)
|
||||
v = v && v !== '0' ? true : false;
|
||||
else
|
||||
v = null;
|
||||
break;
|
||||
case 'array':
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_array(v, type, sub)
|
||||
{
|
||||
let error = false;
|
||||
if (!(v instanceof Array) && v != null && type.allow_scalar !== false)
|
||||
v = [ v ];
|
||||
if (v != null)
|
||||
|
@ -90,7 +144,7 @@ class ServerPropTypes
|
|||
if (type.items)
|
||||
{
|
||||
if (v.length != type.items.length)
|
||||
bad = true;
|
||||
error = true;
|
||||
else
|
||||
{
|
||||
let rv = [];
|
||||
|
@ -99,7 +153,7 @@ class ServerPropTypes
|
|||
let st = this.check(v[i], type.items[i], sub);
|
||||
if (st.error)
|
||||
{
|
||||
bad = {
|
||||
error = {
|
||||
path: [ i, ...(st.error.path||[]) ],
|
||||
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
|
||||
};
|
||||
|
@ -116,7 +170,7 @@ class ServerPropTypes
|
|||
type.minLength != null && v.length < type.minLength ||
|
||||
type.required && v.length == 0)
|
||||
{
|
||||
bad = true;
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -127,7 +181,7 @@ class ServerPropTypes
|
|||
let st = this.check(v[i], type_of, sub);
|
||||
if (st.error)
|
||||
{
|
||||
bad = {
|
||||
error = {
|
||||
path: [ i, ...(st.error.path||[]) ],
|
||||
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
|
||||
};
|
||||
|
@ -141,17 +195,21 @@ class ServerPropTypes
|
|||
}
|
||||
else if (type.required)
|
||||
{
|
||||
bad = true;
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
v = null;
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_object(v, type, sub)
|
||||
{
|
||||
let error = false;
|
||||
if (v != null && typeof v != 'object' || v instanceof Array || type.required && !v)
|
||||
{
|
||||
bad = true;
|
||||
error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -163,7 +221,7 @@ class ServerPropTypes
|
|||
let st = this.check(v[k], type.items[k], sub);
|
||||
if (st.error)
|
||||
{
|
||||
bad = {
|
||||
error = {
|
||||
path: [ k, ...(st.error.path||[]) ],
|
||||
name: (type.comment
|
||||
? type.comment + (st.error.name ? ' - '+st.error.name : '')
|
||||
|
@ -184,7 +242,7 @@ class ServerPropTypes
|
|||
{
|
||||
if (re && !re.exec(k))
|
||||
{
|
||||
bad = true;
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
if (type.of)
|
||||
|
@ -192,7 +250,7 @@ class ServerPropTypes
|
|||
let st = this.check(v[k], type.of, sub);
|
||||
if (st.error)
|
||||
{
|
||||
bad = {
|
||||
error = {
|
||||
path: [ k, ...(st.error.path||[]) ],
|
||||
name: (type.comment
|
||||
? type.comment + ' - ' + k + (st.error.name ? ' - '+st.error.name : '')
|
||||
|
@ -208,12 +266,20 @@ class ServerPropTypes
|
|||
v = nv;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'enum':
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_enum(v, type)
|
||||
{
|
||||
let error = false;
|
||||
if (!type.of.filter(o => o === v).length)
|
||||
bad = true;
|
||||
break;
|
||||
case 'one':
|
||||
error = true;
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static check_one(v, type, sub)
|
||||
{
|
||||
let error = false;
|
||||
let type_of = this.expandOneOf(type, sub);
|
||||
for (let i = 0; i < type_of.length; i++)
|
||||
{
|
||||
|
@ -222,18 +288,18 @@ class ServerPropTypes
|
|||
if (!st.error)
|
||||
{
|
||||
v = st.value;
|
||||
bad = false;
|
||||
error = false;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
bad = bad || true;
|
||||
error = error || true;
|
||||
if (ti.type == 'object' && ti.items && typeof v == 'object')
|
||||
{
|
||||
let fit_enum;
|
||||
for (const k in ti.items)
|
||||
{
|
||||
if (ti.items[k].type == 'enum')
|
||||
if (ti.items[k].type == 'enum' || typeof ti.items[k] != 'object')
|
||||
{
|
||||
fit_enum = fit_enum || {};
|
||||
fit_enum[k] = ti.items[k];
|
||||
|
@ -245,45 +311,22 @@ class ServerPropTypes
|
|||
if (!fitst.error)
|
||||
{
|
||||
const fit_fields = Object.keys(ti.items).filter(k => ti.items[k].required && (k in v)).length;
|
||||
if (!bad || bad === true || bad.fields < fit_fields)
|
||||
if (!error || error === true || error.fields < fit_fields)
|
||||
{
|
||||
bad = { fields: fit_fields, error: st.error };
|
||||
error = { fields: fit_fields, error: st.error };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bad && bad !== true)
|
||||
if (error && error !== true)
|
||||
{
|
||||
bad = bad.error;
|
||||
error = error.error;
|
||||
if (type.comment)
|
||||
bad.name = type.comment + (bad.name ? ' - ' + bad.name : '');
|
||||
error.name = type.comment + (error.name ? ' - ' + error.name : '');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown-type');
|
||||
}
|
||||
if (bad)
|
||||
{
|
||||
if (bad === true)
|
||||
{
|
||||
bad = {
|
||||
path: [],
|
||||
name: type.comment,
|
||||
};
|
||||
}
|
||||
if (options && options.varname)
|
||||
{
|
||||
bad.name = options.varname + (bad.name ? ' - ' + bad.name : '');
|
||||
}
|
||||
if (options && options.return_error)
|
||||
{
|
||||
return { error: bad };
|
||||
}
|
||||
throw new UserError('invalid-format', bad);
|
||||
}
|
||||
return options && options.return_error ? { value: v } : v;
|
||||
return { value: v, error };
|
||||
}
|
||||
|
||||
static expandOneOf(type, options)
|
||||
|
@ -291,7 +334,9 @@ class ServerPropTypes
|
|||
const type_of = [];
|
||||
for (let o of type.of)
|
||||
{
|
||||
o = this.fill(type.required ? { ...o, required: true } : o, options);
|
||||
o = this.fill(o, options);
|
||||
if (type.required)
|
||||
o = { ...o, required: true };
|
||||
if (o.type == 'one')
|
||||
type_of.push.apply(type_of, this.expandOneOf(o, options));
|
||||
else
|
||||
|
@ -324,12 +369,9 @@ class ServerPropTypes
|
|||
type = { type: 'object', items: type };
|
||||
}
|
||||
}
|
||||
else if (typeof type == 'string')
|
||||
{
|
||||
type = { type };
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scalar is a fixed value
|
||||
type = { type: 'enum', of: [ type ] };
|
||||
}
|
||||
if (type.type == 'ref')
|
||||
|
@ -351,6 +393,12 @@ class ServerPropTypes
|
|||
}
|
||||
}
|
||||
|
||||
const common = {
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
};
|
||||
|
||||
ServerPropTypes.metaType = {
|
||||
refs: {
|
||||
anytype: {
|
||||
|
@ -359,113 +407,86 @@ ServerPropTypes.metaType = {
|
|||
{ type: 'ref', ref: 'spt' },
|
||||
{ type: 'array', of: { type: 'ref', ref: 'spt' } },
|
||||
{ type: 'object', of: { type: 'ref', ref: 'spt' } },
|
||||
{ type: 'enum', of: [ 'any', 'string', 'decimal', 'num', 'int', 'id', 'bool', 'array', 'object' ] },
|
||||
{ type: 'num' },
|
||||
{ type: 'bool' },
|
||||
{ type: 'string' },
|
||||
],
|
||||
},
|
||||
spt: {
|
||||
type: 'one',
|
||||
of: [
|
||||
{ comment: 'Reference to another type', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'ref' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'ref',
|
||||
...common,
|
||||
ref: { type: 'string', comment: 'Type reference', required: true },
|
||||
} },
|
||||
{ comment: 'Any type', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'any' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'any',
|
||||
...common,
|
||||
} },
|
||||
{ comment: 'String', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'string' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'string',
|
||||
...common,
|
||||
regex: { type: 'string', comment: 'Regular expression' },
|
||||
regexOptions: { type: 'string', comment: 'Regular expression flags' },
|
||||
} },
|
||||
{ comment: 'String-coded decimal', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'decimal' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'decimal',
|
||||
...common,
|
||||
decimals: { type: 'int', comment: 'Allowed decimal places' },
|
||||
} },
|
||||
{ comment: 'Number', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'num' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'num',
|
||||
...common,
|
||||
min: { type: 'num', comment: 'Minimum value' },
|
||||
max: { type: 'num', comment: 'Maximum value' },
|
||||
} },
|
||||
{ comment: 'Integer', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'int' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'int',
|
||||
...common,
|
||||
min: { type: 'int', comment: 'Minimum value' },
|
||||
max: { type: 'int', comment: 'Maximum value' },
|
||||
} },
|
||||
{ comment: 'ID (positive integer or NULL)', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'id' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'id',
|
||||
...common,
|
||||
} },
|
||||
{ comment: 'Boolean', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'bool' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'bool',
|
||||
...common,
|
||||
} },
|
||||
{ comment: 'Array', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'array' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'array',
|
||||
...common,
|
||||
of: { type: 'ref', ref: 'anytype', required: true, comment: 'Array item type' },
|
||||
minLength: { type: 'int', comment: 'Minimum length' },
|
||||
maxLength: { type: 'int', comment: 'Maximum length' },
|
||||
} },
|
||||
{ comment: 'Tuple (fixed-length array)', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'array' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'array',
|
||||
...common,
|
||||
items: { type: 'array', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Tuple item types' },
|
||||
} },
|
||||
{ comment: 'Unstructured hash', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'object' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'object',
|
||||
...common,
|
||||
of: { type: 'ref', ref: 'anytype', required: true, comment: 'Hash item type' },
|
||||
keyRegex: { type: 'string', comment: 'Hash key regular expression' },
|
||||
keyRegexOptions: { type: 'string', comment: 'Hash key regular expression flags' },
|
||||
} },
|
||||
{ comment: 'Structured object', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'object' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'object',
|
||||
...common,
|
||||
items: { type: 'object', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Field types' },
|
||||
} },
|
||||
{ comment: 'Enum of constants', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'enum' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
of: { type: 'array', of: 'any', minLength: 1, required: true, comment: 'Constants' },
|
||||
type: 'enum',
|
||||
...common,
|
||||
of: { type: 'array', of: { type: 'any' }, minLength: 1, required: true, comment: 'Constants' },
|
||||
} },
|
||||
{ comment: 'One of types', type: 'object', items: {
|
||||
type: { type: 'enum', of: [ 'one' ] },
|
||||
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } },
|
||||
required: { type: 'bool', comment: 'Item is required' },
|
||||
comment: { type: 'string', comment: 'Item comment' },
|
||||
type: 'one',
|
||||
...common,
|
||||
of: { type: 'array', of: { type: 'ref', ref: 'anytype' }, minLength: 1, required: true, comment: 'Type options' },
|
||||
} },
|
||||
],
|
||||
|
|
3
test.js
3
test.js
|
@ -13,4 +13,7 @@ const type = {
|
|||
console.log(JSON.stringify(spt.check({ type: 'object', items: type }, spt.metaType, { return_error: true }), null, 2));
|
||||
|
||||
// Self-validate!
|
||||
let r = {};
|
||||
for (let i = 0; i < 1000; i++)
|
||||
r = spt.check(spt.metaType, spt.metaType, { return_error: true });
|
||||
console.log(JSON.stringify(spt.check(spt.metaType, spt.metaType, { return_error: true }), null, 2));
|
||||
|
|
Loading…
Reference in New Issue