"use strict" ;
const createError = require ( "create-error" ) ;
const assureArray = require ( "assure-array" ) ;
const defaultValue = require ( "default-value" ) ;
const util = require ( "util" ) ;
let ValidationError = createError ( "ValidationError" , { path : [ ] } ) ;
function allowExtraProperties ( rules ) {
return function ( value ) {
return validateObject ( value , rules , true ) ;
} ;
}
function validateValue ( value , argRules ) {
let argRules _ = assureArray ( argRules ) ;
let isRequired = argRules _ . some ( ( rule ) => rule . _ _ _validationMarker === "required" ) ;
if ( isRequired && value == null ) {
return [ new ValidationError ( ` Required value is missing ` ) ] ;
} else if ( value != null ) {
let actualRules = argRules _ . filter ( ( rule ) => {
return ( rule . _ _ _validationMarker == null ) ;
} ) ;
let errors = [ ] ;
for ( let rule of actualRules ) {
if ( typeof rule === "object" ) {
/* FIXME: Check that this isn't an array, Date, Buffer, ... */
errors = validateObject ( value , rule ) ;
} else {
try {
errors = defaultValue ( rule ( value ) , [ ] ) ;
} catch ( error ) {
if ( error instanceof ValidationError ) {
errors = [ error ] ;
}
}
}
if ( errors . length > 0 ) {
break ;
}
}
return errors ;
}
}
function validateObject ( object , rules , allowExtra = false ) {
let errors = [ ] ;
if ( ! allowExtra ) {
errors = Object . keys ( object ) . map ( ( propertyName ) => {
if ( rules [ propertyName ] == null ) {
return new ValidationError ( ` Encountered an unexpected property ' ${ propertyName } ' ` ) ;
} else {
return null ;
}
} ) . filter ( ( error ) => {
return ( error != null ) ;
} ) ;
}
if ( errors . length > 0 ) {
return errors ;
} else {
return Object . keys ( rules ) . map ( ( key ) => {
let errors = validateValue ( object [ key ] , rules [ key ] ) ;
return annotateErrors ( key , object [ key ] , assureArray ( errors ) ) ;
} ) . reduce ( ( allErrors , errors ) => {
return allErrors . concat ( errors ) ;
} , [ ] ) ;
}
}
function validateArgumentList ( args , rules ) {
if ( args . length > rules . length ) {
return [ new ValidationError ( ` Got ${ args . length } arguments, but only expected ${ rules . length } ` ) ] ;
} else {
return rules . map ( ( item , i ) => {
let arg = args [ i ] ;
let argName = item [ 0 ] ;
let argRules = item . slice ( 1 ) ;
if ( typeof argName !== "string" ) {
throw new Error ( "First item in the argument rules list must be the argument name" ) ;
} else {
let errors = validateValue ( arg , argRules ) ;
return annotateErrors ( argName , arg , assureArray ( errors ) ) ;
}
} ) . reduce ( ( allErrors , errors ) => {
return allErrors . concat ( errors ) ;
} , [ ] ) ;
}
}
function annotateErrors ( pathSegment , value , errors ) {
return errors . map ( ( error ) => {
error . path = assureArray ( pathSegment ) . concat ( error . path ) ;
if ( error . value == null ) {
error . value = value ;
}
return error ;
} ) ;
}
function aggregrateErrors ( errors ) {
let rephrasedErrors = errors . map ( ( error ) => {
/* TODO: Make immutable */
let path = ( error . path . length > 0 )
? error . path . join ( " -> " )
: "(root)" ;
error . message = ` At ${ path } : ${ error . message } ` ;
return error ;
} ) ;
let detailLines = rephrasedErrors . map ( ( error ) => {
return ` - ${ error . message } ` ;
} ) . join ( "\n" ) ;
if ( errors . length > 0 ) {
throw new ValidationError ( ` One or more validation errors occurred: \n ${ detailLines } ` , {
errors : rephrasedErrors
} ) ;
}
}
function validateArguments ( args , rules ) {
let errors = validateArgumentList ( args , rules ) ;
aggregrateErrors ( errors ) ;
}
module . exports = {
validateArguments : validateArguments ,
validateValue : function ( value , rules ) {
let errors = validateValue ( value , assureArray ( rules ) ) ;
aggregrateErrors ( errors ) ;
} ,
validateOptions : function ( args , optionsRules ) {
return validateArguments ( args , [
[ "options" , optionsRules ]
] ) ;
} ,
ValidationError : ValidationError ,
required : { _ _ _validationMarker : "required" } ,
isFunction : function ( value ) {
if ( typeof value !== "function" ) {
throw new ValidationError ( "Must be a function" ) ;
}
} ,
isString : function ( value ) {
if ( typeof value !== "string" ) {
throw new ValidationError ( "Must be a string" ) ;
}
} ,
isNumber : function ( value ) {
if ( typeof value !== "number" ) {
throw new ValidationError ( "Must be a number" ) ;
}
} ,
isBoolean : function ( value ) {
if ( typeof value !== "boolean" ) {
throw new ValidationError ( "Must be a boolean" ) ;
}
} ,
isDate : function ( value ) {
if ( ! ( value instanceof Date ) ) {
throw new ValidationError ( "Must be a Date object" ) ;
}
} ,
isBuffer : function ( value ) {
if ( ! ( value instanceof Buffer ) ) {
throw new ValidationError ( "Must be a Buffer object" ) ;
}
} ,
either : function ( ... alternatives ) {
if ( alternatives . length === 0 ) {
throw new Error ( "Must specify at least one alternative" ) ;
} else {
return function ( value ) {
let errors = [ ] ;
for ( let alternative of alternatives ) {
let result = validateValue ( value , alternative ) ;
if ( result . length === 0 ) {
return ;
} else {
errors = errors . concat ( result ) ;
}
}
let errorList = errors . map ( ( error ) => {
return ` " ${ error . message } " ` ;
} ) . join ( ", " ) ;
return new ValidationError ( ` Must satisfy at least one of: ${ errorList } ` , { errors : errors } ) ;
} ;
}
} ,
when : function ( predicate , rules ) {
if ( rules == null ) {
throw new Error ( "No rules specified for a `when` validation clause; did you misplace a parenthese?" ) ;
} else {
let rules _ = assureArray ( rules ) . map ( ( rule ) => {
if ( typeof rule === "object" ) {
/* We automatically allow extraneous properties in a `when` clause, because it'll generally be used as a partial addition to an otherwise-fully-specified object structure. */
return allowExtraProperties ( rule ) ;
} else {
return rule ;
}
} ) ;
return function ( value ) {
let matches = predicate ( value ) ;
if ( matches ) {
return validateValue ( value , rules _ ) ;
} else {
return [ ] ;
}
} ;
}
} ,
matchesFormat : function ( regex ) {
return function ( value ) {
if ( ! regex . test ( value ) ) {
throw new ValidationError ( ` Must match format: ${ regex } ` ) ;
}
} ;
} ,
oneOf : function ( validValues ) {
if ( Array . isArray ( validValues ) ) {
let validValueSet = new Set ( validValues ) ;
return function ( value ) {
if ( ! validValueSet . has ( value ) ) {
throw new ValidationError ( ` Must be one of: ${ validValues . map ( ( item ) => util . inspect ( item ) ) . join ( ", " ) } ` ) ;
}
}
} else {
throw new Error ( "Argument to `oneOf` must be an array of values" ) ;
}
} ,
arrayOf : function ( rules ) {
let rules _ = assureArray ( rules ) ;
return function ( value ) {
if ( ! Array . isArray ( value ) ) {
throw new ValidationError ( "Must be an array" ) ;
} else {
return value . map ( ( item , i ) => {
let errors = validateValue ( item , rules _ ) ;
return annotateErrors ( i , item , assureArray ( errors ) ) ;
} ) . reduce ( ( allErrors , errors ) => {
return allErrors . concat ( errors ) ;
} , [ ] ) ;
}
} ;
} ,
anyProperty : function ( rules ) {
let keyRules = assureArray ( defaultValue ( rules . key , [ ] ) ) ;
let valueRules = assureArray ( defaultValue ( rules . value , [ ] ) ) ;
return function ( object ) {
if ( typeof object !== "object" || Array . isArray ( object ) ) {
throw new ValidationError ( "Must be an object" ) ;
} else {
return Object . keys ( object ) . map ( ( key ) => {
let value = object [ key ] ;
let keyErrors = validateValue ( key , keyRules ) ;
let valueErrors = validateValue ( value , valueRules ) ;
let annotatedKeyErrors = annotateErrors ( [ key , "(key)" ] , key , assureArray ( keyErrors ) ) ;
let annotatedValueErrors = annotateErrors ( [ key , "(value)" ] , value , assureArray ( valueErrors ) ) ;
return annotatedKeyErrors . concat ( annotatedValueErrors ) ;
} ) . reduce ( ( allErrors , errors ) => {
return allErrors . concat ( errors ) ;
} , [ ] ) ;
}
} ;
} ,
allowExtraProperties : allowExtraProperties ,
forbidden : function ( value ) {
if ( value != null ) {
throw new ValidationError ( "Value exists in a place that should be empty" ) ;
}
}
} ;