/** * Yet another omnivorous data validator * * Peculiar features: * - 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 itself (unlike JSON Schema and Joi too) * * (c) Vitaliy Filippov 2019+ * * Version 2019-09-22 */ 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); const fn = this['check_'+type.type]; if (!fn) { 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 === '')) 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) { const type_of = []; for (let o of type.of) { 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 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 { // Scalar is a fixed value 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; } } 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: { type: 'one', of: [ { type: 'ref', ref: 'spt' }, { type: 'array', of: { type: 'ref', ref: 'spt' } }, { type: 'object', of: { type: 'ref', ref: 'spt' } }, { type: 'num' }, { type: 'bool' }, { type: 'string' }, ], }, spt: { type: 'one', of: [ { comment: 'Reference to another type', type: 'object', items: { type: 'ref', ...common, ref: { type: 'string', comment: 'Type reference', required: true }, } }, { comment: 'Any type', type: 'object', items: { type: 'any', ...common, } }, { comment: 'String', type: 'object', items: { type: 'string', ...common, regex: { type: 'string', comment: 'Regular expression' }, regexOptions: { type: 'string', comment: 'Regular expression flags' }, } }, { comment: 'String-coded decimal', type: 'object', items: { type: 'decimal', ...common, decimals: { type: 'int', comment: 'Allowed decimal places' }, } }, { comment: 'Number', type: 'object', items: { type: 'num', ...common, min: { type: 'num', comment: 'Minimum value' }, max: { type: 'num', comment: 'Maximum value' }, } }, { comment: 'Integer', type: 'object', items: { 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: 'id', ...common, } }, { comment: 'Boolean', type: 'object', items: { type: 'bool', ...common, } }, { comment: 'Array', type: 'object', items: { 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: 'array', ...common, items: { type: 'array', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Tuple item types' }, } }, { comment: 'Unstructured hash', type: 'object', items: { 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: 'object', ...common, items: { type: 'object', of: { type: 'ref', ref: 'anytype' }, required: true, comment: 'Field types' }, } }, { comment: 'Enum of constants', type: 'object', items: { type: 'enum', ...common, of: { type: 'array', of: { type: 'any' }, minLength: 1, required: true, comment: 'Constants' }, } }, { comment: 'One of types', type: 'object', items: { type: 'one', ...common, of: { type: 'array', of: { type: 'ref', ref: 'anytype' }, minLength: 1, required: true, comment: 'Type options' }, } }, ], }, }, type: 'ref', ref: 'spt', }; module.exports = ServerPropTypes;