Initial commit
commit
9a67ab46ee
|
@ -0,0 +1,457 @@
|
||||||
|
/**
|
||||||
|
* Yet another omnivorous data validator
|
||||||
|
*
|
||||||
|
* Peculiar features:
|
||||||
|
* - Uses literal definitions (unlike, for example, Joi)
|
||||||
|
* - Simpler than JSON schema, but convertible to it
|
||||||
|
* - Supports comments and convenient user-readable validation errors
|
||||||
|
* - Coerces decimal strings to numbers
|
||||||
|
* - Coerces scalars to single-element arrays
|
||||||
|
*
|
||||||
|
* (c) Vitaliy Filippov 2019+
|
||||||
|
*
|
||||||
|
* Version 2019-09-21
|
||||||
|
*/
|
||||||
|
const UserError = require('./UserError.js');
|
||||||
|
|
||||||
|
class ServerPropTypes
|
||||||
|
{
|
||||||
|
static check(v, type, options)
|
||||||
|
{
|
||||||
|
const sub = {
|
||||||
|
refs: options && options.refs ? (type.refs ? { ...options.refs, ...type.refs } : options.refs) : type.refs,
|
||||||
|
return_error: true,
|
||||||
|
};
|
||||||
|
type = this.fill(type, sub);
|
||||||
|
let bad = false;
|
||||||
|
switch (type.type)
|
||||||
|
{
|
||||||
|
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 + (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 + (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 (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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static expandOneOf(type, options)
|
||||||
|
{
|
||||||
|
const type_of = [];
|
||||||
|
for (let o of type.of)
|
||||||
|
{
|
||||||
|
o = this.fill(type.required ? { ...o, required: true } : o, options);
|
||||||
|
if (o.type == 'one')
|
||||||
|
type_of.push.apply(type_of, this.expandOneOf(o, options));
|
||||||
|
else
|
||||||
|
type_of.push(o);
|
||||||
|
}
|
||||||
|
return type_of;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fill(type, options)
|
||||||
|
{
|
||||||
|
if (type instanceof Array)
|
||||||
|
{
|
||||||
|
if (type.length == 1)
|
||||||
|
{
|
||||||
|
type = { type: 'array', of: type[0] };
|
||||||
|
}
|
||||||
|
else if (!type.length)
|
||||||
|
{
|
||||||
|
type = { type: 'array', of: 'any' };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = { type: 'array', items: type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (typeof type == 'object')
|
||||||
|
{
|
||||||
|
if (!type.type)
|
||||||
|
{
|
||||||
|
type = { type: 'object', items: type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (typeof type == 'string')
|
||||||
|
{
|
||||||
|
type = { type };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = { type: 'enum', of: [ type ] };
|
||||||
|
}
|
||||||
|
if (type.type == 'ref')
|
||||||
|
{
|
||||||
|
if (!options || !options.refs || !options.refs[type.ref])
|
||||||
|
{
|
||||||
|
throw new UserError('ref-missing', { ref: type.ref });
|
||||||
|
}
|
||||||
|
if (type.required || type.comment)
|
||||||
|
{
|
||||||
|
type = { ...options.refs[type.ref], required: type.required, comment: type.comment };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = options.refs[type.ref];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerPropTypes.metaType = {
|
||||||
|
refs: {
|
||||||
|
anytype: {
|
||||||
|
type: 'one',
|
||||||
|
of: [
|
||||||
|
{ 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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
spt: {
|
||||||
|
type: 'one',
|
||||||
|
of: [
|
||||||
|
{ comment: 'Any type', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'any' ] },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
} },
|
||||||
|
{ comment: 'String', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'string' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
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' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
decimals: { type: 'int', comment: 'Allowed decimal places' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Number', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'num' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
min: { type: 'num', comment: 'Minimum value' },
|
||||||
|
max: { type: 'num', comment: 'Maximum value' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Integer', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'int' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
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' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Boolean', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'bool' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Array', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'array' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
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' ] },
|
||||||
|
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' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Unstructured hash', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'object' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
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' ] },
|
||||||
|
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' },
|
||||||
|
} },
|
||||||
|
{ comment: 'Enum of constants', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'enum' ] },
|
||||||
|
required: { type: 'bool', comment: 'Item is required' },
|
||||||
|
comment: { type: 'string', comment: 'Item comment' },
|
||||||
|
of: { type: 'array', minLength: 1, required: true, comment: 'Constants' },
|
||||||
|
} },
|
||||||
|
{ comment: 'One of types', type: 'object', items: {
|
||||||
|
type: { type: 'enum', of: [ 'one' ] },
|
||||||
|
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' },
|
||||||
|
} },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'ref',
|
||||||
|
ref: 'spt',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ServerPropTypes;
|
|
@ -0,0 +1,15 @@
|
||||||
|
class UserError extends Error
|
||||||
|
{
|
||||||
|
constructor(code, props)
|
||||||
|
{
|
||||||
|
super(code);
|
||||||
|
this.props = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString()
|
||||||
|
{
|
||||||
|
return this.message+(this.props ? ' '+JSON.stringify(this.props, null, 4) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserError;
|
|
@ -0,0 +1,13 @@
|
||||||
|
const spt = require('./ServerPropTypes.js');
|
||||||
|
|
||||||
|
const type = {
|
||||||
|
segment_id: { type: 'array', of: 'int', comment: 'Сегмент(ы)' },
|
||||||
|
init_area: { type: 'num', min: 0.1, required: true, comment: 'Начальное отклонение площади объекта' },
|
||||||
|
search_order: { type: 'array', of: {
|
||||||
|
field: { type: 'enum', of2: [ 'area', 'radius', 'pubdate' ], required: true, comment: 'Поле' },
|
||||||
|
step: { type: 'num', required: true, comment: 'Шаг' },
|
||||||
|
iter: { type: 'int', required: true, comment: 'Число итераций' },
|
||||||
|
}, comment: 'Порядок перебора параметров' },
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(spt.check({ type: 'object', items: type }, spt.metaType, { return_error: true }), null, 2));
|
Loading…
Reference in New Issue