Split into separate methods, make literal string mean const

master
Vitaliy Filippov 2019-09-22 13:59:51 +03:00
parent fd140bfa31
commit 2122dc7288
2 changed files with 334 additions and 310 deletions

View File

@ -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,266 +24,309 @@ 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':
if (v != null && typeof v != 'number' && typeof v != 'string' || type.required && (v == null || v === ''))
bad = true;
else
{
v = ''+v;
if (type.regex && !new RegExp(type.regex, type.regexOptions).exec(v))
bad = true;
}
break;
case 'decimal':
if (v != null && typeof v != 'number' && typeof v != 'string' || type.required && (v == null || v == 0))
bad = 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;
}
break;
case 'num':
v = v == null ? null : parseFloat(v);
if (v != v)
bad = true;
else if (type.min != null && v < type.min || type.max != null && v > type.max)
bad = true;
else if (type.required && !v)
bad = true;
break;
case 'int':
v = v == null ? null : parseInt(v);
if (v != v)
bad = true;
else if (type.min != null && v < type.min || type.max != null && v > type.max)
bad = true;
else if (type.required && !v)
bad = true;
break;
case 'id':
v = v ? parseInt(v) : null;
if (v != v || v < 0 || v == null && type.required)
bad = true;
break;
case 'bool':
if ((v != null || type.required) && v !== false && v !== true &&
v !== '' && v !== 0 && v !== '0' && v !== 1 && v !== '1')
bad = true;
if (v != null)
v = v && v !== '0' ? true : false;
else
v = null;
break;
case 'array':
if (!(v instanceof Array) && v != null && type.allow_scalar !== false)
v = [ v ];
if (v != null)
{
if (type.items)
{
if (v.length != type.items.length)
bad = true;
else
{
let rv = [];
for (let i = 0; i < type.items.length; i++)
{
let st = this.check(v[i], type.items[i], sub);
if (st.error)
{
bad = {
path: [ i, ...(st.error.path||[]) ],
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
};
break;
}
rv[i] = st.value;
}
v = rv;
}
}
else if (type.of)
{
if (type.maxLength != null && v.length > type.maxLength ||
type.minLength != null && v.length < type.minLength ||
type.required && v.length == 0)
{
bad = true;
}
else
{
let rv = [];
const type_of = this.fill(type.of, sub);
for (let i = 0; i < v.length; i++)
{
let st = this.check(v[i], type_of, sub);
if (st.error)
{
bad = {
path: [ i, ...(st.error.path||[]) ],
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
};
break;
}
rv[i] = st.value;
}
v = rv;
}
}
}
else if (type.required)
{
bad = true;
}
else
{
v = null;
}
break;
case 'object':
if (v != null && typeof v != 'object' || v instanceof Array || type.required && !v)
{
bad = true;
}
else
{
if (type.items)
{
let nv = {};
for (let k in type.items)
{
let st = this.check(v[k], type.items[k], sub);
if (st.error)
{
bad = {
path: [ k, ...(st.error.path||[]) ],
name: (type.comment
? type.comment + (st.error.name ? ' - '+st.error.name : '')
: st.error.name),
};
break;
}
if (k in v)
nv[k] = st.value;
}
v = nv;
}
else if (type.of || type.keyRegex)
{
let re = type.keyRegex ? new RegExp(type.keyRegex, type.keyRegexOptions) : null;
let nv = {};
for (let k in v)
{
if (re && !re.exec(k))
{
bad = true;
break;
}
if (type.of)
{
let st = this.check(v[k], type.of, sub);
if (st.error)
{
bad = {
path: [ k, ...(st.error.path||[]) ],
name: (type.comment
? type.comment + ' - ' + k + (st.error.name ? ' - '+st.error.name : '')
: k + (st.error.name ? ' - '+st.error.name : '')),
};
break;
}
nv[k] = st.value;
}
else
nv[k] = v[k];
}
v = nv;
}
}
break;
case 'enum':
if (!type.of.filter(o => o === v).length)
bad = true;
break;
case 'one':
let type_of = this.expandOneOf(type, sub);
for (let i = 0; i < type_of.length; i++)
{
const ti = type_of[i];
const st = this.check(v, ti, sub);
if (!st.error)
{
v = st.value;
bad = false;
break;
}
else
{
bad = bad || 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')
{
fit_enum = fit_enum || {};
fit_enum[k] = ti.items[k];
}
}
if (fit_enum)
{
let fitst = this.check(v, { type: 'object', items: fit_enum }, sub);
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)
{
bad = { fields: fit_fields, error: st.error };
}
}
}
}
}
}
if (bad && bad !== true)
{
bad = bad.error;
if (type.comment)
bad.name = type.comment + (bad.name ? ' - ' + bad.name : '');
}
break;
default:
throw new Error('unknown-type');
throw new Error('unknown-type');
}
if (bad)
const r = fn.call(this, v, type, sub);
if (r.error)
{
if (bad === true)
if (r.error === true)
{
bad = {
r.error = {
path: [],
name: type.comment,
};
}
if (options && options.varname)
{
bad.name = options.varname + (bad.name ? ' - ' + bad.name : '');
r.error.name = options.varname + (r.error.name ? ' - ' + r.error.name : '');
}
if (options && options.return_error)
{
return { error: bad };
return { error: r.error };
}
throw new UserError('invalid-format', bad);
throw new UserError('invalid-format', r.error);
}
return options && options.return_error ? { value: v } : v;
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 === ''))
error = true;
else
{
v = ''+v;
if (type.regex && !new RegExp(type.regex, type.regexOptions).exec(v))
error = true;
}
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))
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)
error = true;
}
return { value: v, error };
}
static check_num(v, type)
{
let error = false;
v = v == null ? null : parseFloat(v);
if (v != v)
error = true;
else if (type.min != null && v < type.min || type.max != null && v > type.max)
error = true;
else if (type.required && !v)
error = true;
return { value: v, error };
}
static check_int(v, type)
{
let error = false;
v = v == null ? null : parseInt(v);
if (v != v)
error = true;
else if (type.min != null && v < type.min || type.max != null && v > type.max)
error = true;
else if (type.required && !v)
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)
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')
error = true;
if (v != null)
v = v && v !== '0' ? true : false;
else
v = null;
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)
{
if (type.items)
{
if (v.length != type.items.length)
error = true;
else
{
let rv = [];
for (let i = 0; i < type.items.length; i++)
{
let st = this.check(v[i], type.items[i], sub);
if (st.error)
{
error = {
path: [ i, ...(st.error.path||[]) ],
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
};
break;
}
rv[i] = st.value;
}
v = rv;
}
}
else if (type.of)
{
if (type.maxLength != null && v.length > type.maxLength ||
type.minLength != null && v.length < type.minLength ||
type.required && v.length == 0)
{
error = true;
}
else
{
let rv = [];
const type_of = this.fill(type.of, sub);
for (let i = 0; i < v.length; i++)
{
let st = this.check(v[i], type_of, sub);
if (st.error)
{
error = {
path: [ i, ...(st.error.path||[]) ],
name: '№'+(i+1) + (st.error.name ? ' - '+st.error.name : ''),
};
break;
}
rv[i] = st.value;
}
v = rv;
}
}
}
else if (type.required)
{
error = true;
}
else
{
v = null;
}
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)
{
error = true;
}
else
{
if (type.items)
{
let nv = {};
for (let k in type.items)
{
let st = this.check(v[k], type.items[k], sub);
if (st.error)
{
error = {
path: [ k, ...(st.error.path||[]) ],
name: (type.comment
? type.comment + (st.error.name ? ' - '+st.error.name : '')
: st.error.name),
};
break;
}
if (k in v)
nv[k] = st.value;
}
v = nv;
}
else if (type.of || type.keyRegex)
{
let re = type.keyRegex ? new RegExp(type.keyRegex, type.keyRegexOptions) : null;
let nv = {};
for (let k in v)
{
if (re && !re.exec(k))
{
error = true;
break;
}
if (type.of)
{
let st = this.check(v[k], type.of, sub);
if (st.error)
{
error = {
path: [ k, ...(st.error.path||[]) ],
name: (type.comment
? type.comment + ' - ' + k + (st.error.name ? ' - '+st.error.name : '')
: k + (st.error.name ? ' - '+st.error.name : '')),
};
break;
}
nv[k] = st.value;
}
else
nv[k] = v[k];
}
v = nv;
}
}
return { value: v, error };
}
static check_enum(v, type)
{
let error = false;
if (!type.of.filter(o => o === v).length)
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++)
{
const ti = type_of[i];
const st = this.check(v, ti, sub);
if (!st.error)
{
v = st.value;
error = false;
break;
}
else
{
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' || typeof ti.items[k] != 'object')
{
fit_enum = fit_enum || {};
fit_enum[k] = ti.items[k];
}
}
if (fit_enum)
{
let fitst = this.check(v, { type: 'object', items: fit_enum }, sub);
if (!fitst.error)
{
const fit_fields = Object.keys(ti.items).filter(k => ti.items[k].required && (k in v)).length;
if (!error || error === true || error.fields < fit_fields)
{
error = { fields: fit_fields, error: st.error };
}
}
}
}
}
}
if (error && error !== true)
{
error = error.error;
if (type.comment)
error.name = type.comment + (error.name ? ' - ' + error.name : '');
}
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' },
} },
],

View File

@ -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));