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 * Yet another omnivorous data validator
* *
* Peculiar features: * Peculiar features:
* - Uses literal definitions (unlike, for example, Joi) * - Uses literal definitions (unlike Joi!)
* - Simpler than JSON schema, but convertible to it * - Simpler than JSON schema, but convertible to it
* - Supports comments and convenient user-readable validation errors * - Supports comments and convenient user-readable validation errors
* - Converts strings to numbers if possible * - Converts strings to numbers if possible
* - Converts scalars to single-element arrays * - Converts scalars to single-element arrays
* - Validates its own meta-type * - Validates itself (unlike JSON Schema and Joi too)
* *
* (c) Vitaliy Filippov 2019+ * (c) Vitaliy Filippov 2019+
* *
* Version 2019-09-21 * Version 2019-09-22
*/ */
const UserError = require('./UserError.js'); const UserError = require('./UserError.js');
@ -24,266 +24,309 @@ class ServerPropTypes
return_error: true, return_error: true,
}; };
type = this.fill(type, sub); type = this.fill(type, sub);
let bad = false; const fn = this['check_'+type.type];
switch (type.type) if (!fn)
{ {
case 'any': throw new Error('unknown-type');
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');
} }
if (bad) const r = fn.call(this, v, type, sub);
if (r.error)
{ {
if (bad === true) if (r.error === true)
{ {
bad = { r.error = {
path: [], path: [],
name: type.comment, name: type.comment,
}; };
} }
if (options && options.varname) 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) 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) static expandOneOf(type, options)
@ -291,7 +334,9 @@ class ServerPropTypes
const type_of = []; const type_of = [];
for (let o of 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') if (o.type == 'one')
type_of.push.apply(type_of, this.expandOneOf(o, options)); type_of.push.apply(type_of, this.expandOneOf(o, options));
else else
@ -324,12 +369,9 @@ class ServerPropTypes
type = { type: 'object', items: type }; type = { type: 'object', items: type };
} }
} }
else if (typeof type == 'string')
{
type = { type };
}
else else
{ {
// Scalar is a fixed value
type = { type: 'enum', of: [ type ] }; type = { type: 'enum', of: [ type ] };
} }
if (type.type == 'ref') 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 = { ServerPropTypes.metaType = {
refs: { refs: {
anytype: { anytype: {
@ -359,113 +407,86 @@ ServerPropTypes.metaType = {
{ type: 'ref', ref: 'spt' }, { type: 'ref', ref: 'spt' },
{ type: 'array', of: { type: 'ref', ref: 'spt' } }, { type: 'array', of: { type: 'ref', ref: 'spt' } },
{ type: 'object', 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: 'num' },
{ type: 'bool' }, { type: 'bool' },
{ type: 'string' },
], ],
}, },
spt: { spt: {
type: 'one', type: 'one',
of: [ of: [
{ comment: 'Reference to another type', type: 'object', items: { { comment: 'Reference to another type', type: 'object', items: {
type: { type: 'enum', of: [ 'ref' ] }, type: 'ref',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
ref: { type: 'string', comment: 'Type reference', required: true }, ref: { type: 'string', comment: 'Type reference', required: true },
} }, } },
{ comment: 'Any type', type: 'object', items: { { comment: 'Any type', type: 'object', items: {
type: { type: 'enum', of: [ 'any' ] }, type: 'any',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
comment: { type: 'string', comment: 'Item comment' },
} }, } },
{ comment: 'String', type: 'object', items: { { comment: 'String', type: 'object', items: {
type: { type: 'enum', of: [ 'string' ] }, type: 'string',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
regex: { type: 'string', comment: 'Regular expression' }, regex: { type: 'string', comment: 'Regular expression' },
regexOptions: { type: 'string', comment: 'Regular expression flags' }, regexOptions: { type: 'string', comment: 'Regular expression flags' },
} }, } },
{ comment: 'String-coded decimal', type: 'object', items: { { comment: 'String-coded decimal', type: 'object', items: {
type: { type: 'enum', of: [ 'decimal' ] }, type: 'decimal',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
decimals: { type: 'int', comment: 'Allowed decimal places' }, decimals: { type: 'int', comment: 'Allowed decimal places' },
} }, } },
{ comment: 'Number', type: 'object', items: { { comment: 'Number', type: 'object', items: {
type: { type: 'enum', of: [ 'num' ] }, type: 'num',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
min: { type: 'num', comment: 'Minimum value' }, min: { type: 'num', comment: 'Minimum value' },
max: { type: 'num', comment: 'Maximum value' }, max: { type: 'num', comment: 'Maximum value' },
} }, } },
{ comment: 'Integer', type: 'object', items: { { comment: 'Integer', type: 'object', items: {
type: { type: 'enum', of: [ 'int' ] }, type: 'int',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
min: { type: 'int', comment: 'Minimum value' }, min: { type: 'int', comment: 'Minimum value' },
max: { type: 'int', comment: 'Maximum value' }, max: { type: 'int', comment: 'Maximum value' },
} }, } },
{ comment: 'ID (positive integer or NULL)', type: 'object', items: { { comment: 'ID (positive integer or NULL)', type: 'object', items: {
type: { type: 'enum', of: [ 'id' ] }, type: 'id',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
} }, } },
{ comment: 'Boolean', type: 'object', items: { { comment: 'Boolean', type: 'object', items: {
type: { type: 'enum', of: [ 'bool' ] }, type: 'bool',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
} }, } },
{ comment: 'Array', type: 'object', items: { { comment: 'Array', type: 'object', items: {
type: { type: 'enum', of: [ 'array' ] }, type: 'array',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
of: { type: 'ref', ref: 'anytype', required: true, comment: 'Array item type' }, of: { type: 'ref', ref: 'anytype', required: true, comment: 'Array item type' },
minLength: { type: 'int', comment: 'Minimum length' }, minLength: { type: 'int', comment: 'Minimum length' },
maxLength: { type: 'int', comment: 'Maximum length' }, maxLength: { type: 'int', comment: 'Maximum length' },
} }, } },
{ comment: 'Tuple (fixed-length array)', type: 'object', items: { { comment: 'Tuple (fixed-length array)', type: 'object', items: {
type: { type: 'enum', of: [ 'array' ] }, type: 'array',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
items: { type: 'array', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Tuple item types' }, items: { type: 'array', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Tuple item types' },
} }, } },
{ comment: 'Unstructured hash', type: 'object', items: { { comment: 'Unstructured hash', type: 'object', items: {
type: { type: 'enum', of: [ 'object' ] }, type: 'object',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
of: { type: 'ref', ref: 'anytype', required: true, comment: 'Hash item type' }, of: { type: 'ref', ref: 'anytype', required: true, comment: 'Hash item type' },
keyRegex: { type: 'string', comment: 'Hash key regular expression' }, keyRegex: { type: 'string', comment: 'Hash key regular expression' },
keyRegexOptions: { type: 'string', comment: 'Hash key regular expression flags' }, keyRegexOptions: { type: 'string', comment: 'Hash key regular expression flags' },
} }, } },
{ comment: 'Structured object', type: 'object', items: { { comment: 'Structured object', type: 'object', items: {
type: { type: 'enum', of: [ 'object' ] }, type: 'object',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
items: { type: 'object', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Field types' }, items: { type: 'object', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Field types' },
} }, } },
{ comment: 'Enum of constants', type: 'object', items: { { comment: 'Enum of constants', type: 'object', items: {
type: { type: 'enum', of: [ 'enum' ] }, type: 'enum',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' }, of: { type: 'array', of: { type: 'any' }, minLength: 1, required: true, comment: 'Constants' },
comment: { type: 'string', comment: 'Item comment' },
of: { type: 'array', of: 'any', minLength: 1, required: true, comment: 'Constants' },
} }, } },
{ comment: 'One of types', type: 'object', items: { { comment: 'One of types', type: 'object', items: {
type: { type: 'enum', of: [ 'one' ] }, type: 'one',
refs: { type: 'object', of: { type: 'ref', ref: 'anytype' } }, ...common,
required: { type: 'bool', comment: 'Item is required' },
comment: { type: 'string', comment: 'Item comment' },
of: { type: 'array', of: { type: 'ref', ref: 'anytype' }, minLength: 1, required: true, comment: 'Type options' }, 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)); console.log(JSON.stringify(spt.check({ type: 'object', items: type }, spt.metaType, { return_error: true }), null, 2));
// Self-validate! // 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)); console.log(JSON.stringify(spt.check(spt.metaType, spt.metaType, { return_error: true }), null, 2));