UNPKG

validate-fields

Version:
362 lines (322 loc) 11.9 kB
'use strict' let Field = require('./Field.js'), Type = require('./Type.js') module.exports = function () { /** Store types defined by a string */ let simpleTypes = Object.create(null) /** Store types defined in terms of other ones */ let typedefs = Object.create(null) /** Store types defined by an object */ let objectTypes = { objects: [], types: [] } /** Store types defined by a callback */ let callbackTypes = { fns: [], types: [] } /** Store types defined by tags */ let taggedTypes = Object.create(null) /** * Validate the given value against the given schema * @param {*} schema * @param {*} value The value can be altered by the validation * @param {Object} [options] Validation options * @returns {boolean} whether the value is valid or nor * @throw if the schema is invalid */ function context(schema, value, options) { schema = context.parse(schema) let ret = schema.validate(value, options) context.lastError = schema.lastError context.lastErrorMessage = schema.lastErrorMessage context.lastErrorPath = schema.lastErrorPath return ret } /** * Store the last error message * @property {string} */ context.lastError = '' /** * Parse the given fields definition * @param {Object} fields * @returns {Field} The parsed object that can be used with validate() * @throw if the definition is invalid */ context.parse = function (fields) { let pos, i, extra, field if (typeof fields === 'string' && fields in simpleTypes) { // Type defined by a string return new Field(simpleTypes[fields]) } else if (typeof fields === 'string' && fields in typedefs) { // Type defined by a string return typedefs[fields] } else if ((pos = objectTypes.objects.indexOf(fields)) !== -1) { // Type defined by an object return new Field(objectTypes.types[pos]) } else if (typeof fields === 'string' && (field = parseTagged(fields, taggedTypes))) { return field } // Search by a type, executing the callbacks for (i = 0; i < callbackTypes.fns.length; i++) { extra = callbackTypes.fns[i](fields, context.parse) if (extra !== undefined) { return new Field(callbackTypes.types[i], extra) } } // Nothing found throw new Error('I couldn\'t understand field definition: ' + fields) } /** * Define a new simple type based on existing types * @param {string} name * @param {*} definition * @param {?checkCallback} preHook * @param {?checkCallback} postHook * @throws if definition could not be understood or this type already exists */ context.typedef = function (name, definition, preHook = null, postHook = null) { if (typeof name !== 'string') { throw new Error('Invalid argument type for name, it must be a string') } definition = context.parse(definition) if (name in simpleTypes || name in typedefs) { throw new Error('Type ' + name + ' already registered') } definition.typedefName = name definition.preHook = preHook definition.postHook = postHook typedefs[name] = definition } /** * @callback parseCallback * @param {*} definition the value being parsed * @param {function} parse the root parse function, used to create recursive definitions * @returns {*} undefined if the definition could not be parsed or a value (any other) to indicate the definition was parsed. The returned value will be sent to checkCallback afterwards */ /** * @callback parseTaggedArgsCallback * @param {Array<string|number|undefined>} args the parsed arguments * @returns {*} will be sent to checkCallback afterwards as extra */ /** * @callback checkCallback * @param {*} value the value to check * @param {*} extra the result of String.prototype.match if the type is defined with a regexp or the parseCallback result if defined with a function * @param {Object} options * @param {string} path a string used in the error message * @returns {*} optionally return the altered value * @throws {string} if the value is invalid, the message will be put in the lastError property of the validation schema */ /** * @callback toJSONCallback * @param {*} extra * @returns {*} */ /** * @callback toJSONSchemaCallback * @param {*} extra * @param {string} componentsPath * @returns {Object} */ /** * Register a new type * @param {(string|Object|RegExp|parseCallback)} definition * @param {string} jsonType One of: number, string, boolean, object, array, *, raw * @param {checkCallback} [checkFn=function(){}] * @param {toJSONCallback|string} [toJSON] - Used only by core types * @param {toJSONSchemaCallback} [toJSONSchema] - Used only by core types */ context.registerType = function (definition, jsonType, checkFn, toJSON, toJSONSchema) { let type = new Type(jsonType, checkFn || (() => {}), toJSON, toJSONSchema) if (typeof definition === 'string') { // Simple definition: match a single string if (definition in simpleTypes || definition in typedefs) { throw new Error('Type ' + definition + ' already registered') } simpleTypes[definition] = type } else if (definition instanceof RegExp) { // Defined by a RegExp: create a callback callbackTypes.fns.push(def2 => { let match if (typeof def2 === 'string' && (match = def2.match(definition))) { return match } }) callbackTypes.types.push(type) } else if (typeof definition === 'object' || (typeof definition === 'function' && typeof definition.name === 'string' && definition.name.match(/^[A-Z]/))) { // Defined by an Object or a constructor if (objectTypes.objects.indexOf(definition) !== -1) { throw new Error('Type ' + definition + ' already registered') } objectTypes.objects.push(definition) objectTypes.types.push(type) } else if (typeof definition === 'function') { // Defined by a callback if (callbackTypes.fns.indexOf(definition) !== -1) { throw new Error('Type ' + definition + ' already registered') } callbackTypes.fns.push(definition) callbackTypes.types.push(type) } else { throw new Error('Invalid definition (' + definition + '), it must be a string, object, regexp or function') } } /** * Register a new type with an Object, used to register with constructors * @param {Object} definition * @param {string} jsonType One of: number, string, boolean, object, array, *, raw * @param {checkCallback} [checkFn=function(){}] * @param {toJSONCallback|string} [toJSON] - Used only by core types * @param {toJSONSchemaCallback} [toJSONSchema] - Used only by core types */ context.registerObjectType = function (definition, jsonType, checkFn, toJSON, toJSONSchema) { let type = new Type(jsonType, checkFn || (() => {}), toJSON, toJSONSchema) if (objectTypes.objects.indexOf(definition) !== -1) { throw new Error('Type ' + definition + ' already registered') } objectTypes.objects.push(definition) objectTypes.types.push(type) } /** * Register a new tagged type * @param {Object} definition * @param {string} definition.tag * @param {string} definition.jsonType One of 'number', 'string', 'boolean', 'object' or 'array' * @param {number} [definition.minArgs=0] * @param {number} [definition.maxArgs=0] Zero means no limit * @param {boolean} [definition.sparse=false] true let some args to be skipped: 'tag(,2,,4)' * @param {boolean} [definition.numeric=false] true will parse all args as numbers * @param {?parseTaggedArgsCallback} [definition.parseArgs=null] process arguments * @param {checkCallback} [checkFn=function(){}] * @param {toJSONCallback|string} [toJSON] - Used only by core types * @param {toJSONSchemaCallback} [toJSONSchema] - Used only by core types */ context.registerTaggedType = function (definition, checkFn, toJSON, toJSONSchema) { if (!definition || typeof definition !== 'object') { throw new Error('Invalid definition (' + definition + '), it must be an object') } else if (!definition.tag || !definition.jsonType) { throw new Error('definition.tag and definition.jsonType are required') } let tag = definition.tag, type = new Type(definition.jsonType, checkFn || (() => {}), toJSON, toJSONSchema) if (tag in taggedTypes) { throw new Error('Tag ' + tag + ' already registered') } taggedTypes[tag] = { type, minArgs: definition.minArgs || 0, maxArgs: definition.maxArgs || 0, sparse: Boolean(definition.sparse), numeric: Boolean(definition.numeric), parseArgs: definition.parseArgs } } /** * Return a reference to all registered types * You can change it to alter the very inner working of this module * The two main types are: * * Hash map: callbackTypes.fns[0], callbackTypes.types[0] * * Array: callbackTypes.fns[1], callbackTypes.types[1] * @returns {{simpleTypes: Object.<string, Type>, typedefs: Field, objectTypes: {objects: Object[], types: Type[]}, callbackTypes: {fns: Function[], types: Type[]}}, taggedTypes: Object<string, {type: string, minArgs: number, maxArgs: number, sparse: boolean, numeric: boolean, parseArgs: ?parseTaggedArgsCallback}>} */ context.getRegisteredTypes = function () { return { simpleTypes, typedefs, objectTypes, callbackTypes, taggedTypes } } // Static link to global reviver context.reviver = module.exports.reviver // Register standard types require('./types')(context) return context } /** * Try to parse a string definition as a tagged type * @private * @param {string} definition * @param {Object<String, Object>} taggedTypes * @returns {?Field} */ function parseTagged(definition, taggedTypes) { let match = definition.match(/^(\w+)(?:\((.*)\))?$/), tag, args, typeInfo if (!match) { return } // Get type info tag = match[1] typeInfo = taggedTypes[tag] if (!typeInfo) { return } // Special no-'()' case: only match 'tag' if minArgs is zero if (typeInfo.minArgs > 0 && match[2] === undefined) { return } // Parse and check args args = (match[2] || '').trim().split(/\s*,\s*/) if (args.length === 1 && args[0] === '') { // Especial empty case args = [] } args = args.map((arg, i) => { if (!arg) { if (!typeInfo.sparse) { throw new Error('Missing argument at position ' + i + ' for tagged type ' + tag) } return } if (typeInfo.numeric) { arg = Number(arg) if (isNaN(arg)) { throw new Error('Invalid numeric argument at position ' + i + ' for tagged type ' + tag) } } return arg }) args.original = definition if (args.length < typeInfo.minArgs) { throw new Error('Too few arguments for tagged type ' + tag) } else if (typeInfo.maxArgs && args.length > typeInfo.maxArgs) { throw new Error('Too many arguments for tagged type ' + tag) } return new Field(typeInfo.type, typeInfo.parseArgs ? typeInfo.parseArgs(args) : args) } let JSONMap = { $Number: Number, $String: String, $Object: Object, $Array: Array, $Boolean: Boolean, $Date: Date } /** * This function is given to be used with JSON.parse() as the 2nd parameter: * parse(JSON.parse(JSON.stringify(aField), reviver)) would give the aField back * @param {string} key * @param {*} value * @returns {*} */ module.exports.reviver = function (key, value) { let match if (typeof value === 'string' && value[0] === '$') { // Special types if (value.indexOf('$RegExp:') === 0) { match = value.match(/^\$RegExp:(.*?):(.*)$/) return new RegExp(match[2], match[1]) } else if (value in JSONMap) { return JSONMap[value] } } return value }