• parambulator.js

  • ¶
    /* Copyright (c) 2012-2015 Richard Rodger, MIT License */
    /* jshint node:true, asi:true, eqnull:true */
    
    (function() {
      "use strict";
    
      var root           = this
      var previous_parambulator = root.parambulator
    
      var has_require = typeof require !== 'undefined'
    
    
      var _      = root._
      var gex    = root.gex
      var jsonic = root.jsonic
    
    
      if( typeof _ === 'undefined' ) {
        if( has_require ) {
          _ = require('lodash')
        }
  • ¶

    assume in web context, so still underscore, not lodash

        else throw new Error('parambulator requires underscore, see http://underscorejs.org');
      }
    
      if( typeof gex === 'undefined' ) {
        if( has_require ) {
          gex = require('gex')
        }
        else throw new Error('parambulator requires gex, see http://github.com/rjrodger/gex');
      }
    
      if( typeof jsonic === 'undefined' ) {
        if( has_require ) {
          jsonic = require('jsonic')
        }
        else throw new Error('parambulator requires jsonic, see http://github.com/rjrodger/jsonic');
      }
    
    
    
      var arrayify = function(){ return Array.prototype.slice.call(arguments[0],arguments[1]) }
    
    
      var quantrule = function( pass, rulename ) {
        rulename = rulename || 'quantrule'
    
        return function(ctxt,cb) {
          ctxt.prop = null
    
          var pn = ctxt.util.proplist(ctxt)
    
          var found = 0
          _.each(pn, function(p){
            found += ctxt.point[p]?1:0
          })
    
          if( !pass(found) ) {
            ctxt.value = ''+pn
            return ctxt.util.fail(ctxt,cb)
          }
          else return cb();
        }
      }
    
      var lenrule = function( pass ) {
        return function(ctxt,cb) {
          var len = ctxt.rule.spec
          var value = ctxt.point
    
          if( !_.isUndefined(value) ) {
            var valuelen = _.isObject(value) ? Object.keys(value).length : value.length
            if ( !_.isUndefined( valuelen ) ){
              if ( !pass( valuelen, len ) ) {
                return ctxt.util.fail(ctxt,cb)
              }
            }
          }
    
          return cb()
        }
      };
    
    
      var childrule = function( pass, noneok, rulename ) {
        rulename = rulename || 'childrule'
    
        return function(ctxt,cb) {
          var pn = ctxt.util.proplist(ctxt)
    
          for( var i = 0; i < pn.length; i++ ) {
            var p = pn[i]
            ctxt.prop = p
    
            var v = ctxt.point[p]
    
            if( !pass(ctxt,p,v) ) {
              ctxt.value = v
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          if( 0 === pn.length ) {
            if( !noneok() ) {
  • ¶

    TODO needs a separate msg code

              ctxt.prop = JSON.stringify(ctxt.rule.spec,killcircles())
              return ctxt.util.fail(ctxt,cb)
            }
          }
          cb();
        }
      }
    
      function proplist(ctxt) {
        var pn = ctxt.rule.spec
  • ¶

    TODO: handle comma separated strings https://github.com/rjrodger/parambulator/issues/19

        if( !_.isArray(pn) ) {
          pn = [''+pn]
        }
    
        var all = []
        _.each(pn, function(n){
    
          if( n.match( /[*?]/ ) ) {
            all = all.concat( _.keys(gex(n).on(ctxt.point)) )
          }
          else all.push(n);
        })
    
        return all
      }
    
    
      function truefn(){return true}
      function falsefn(){return false}
      function noval(v){return _.isUndefined(v)||_.isNull(v)}
    
      var valrule = function( pass ) {
        return function(ctxt,cb) {
          var v = ctxt.point
          var p = ctxt.rule.spec
          if( !_.isUndefined(v) ) {
            if( !pass(v, p) ) {
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          return cb();
        }
      }
    
    
      var rulemap = {
    
        atmostone$:  quantrule( function(f){return f<=1}, 'atmostone$'),
        exactlyone$: quantrule( function(f){return 1==f}, 'exactlyone$'),
        atleastone$: quantrule( function(f){return 1<=f}, 'atleastone$'),
    
    
        required$: childrule( function(ctxt,p,v){return !_.isUndefined(v)}, falsefn, 'required$' ),
    
        notempty$: childrule(
          function(ctxt,p,v){
            return !_.isUndefined(v) && !_.isNull(v) && '' !== v
          },
          truefn,
          'notempty$'
        ),
    
        string$:   childrule( function(ctxt,p,v){return noval(v) || _.isString(v)}, truefn, 'string$' ),
        integer$:  childrule( function(ctxt,p,v){return noval(v) || _.isNumber(v) && v===(v|0)}, truefn, 'integer$' ),
        number$:   childrule( function(ctxt,p,v){return noval(v) || _.isNumber(v)}, truefn, 'number$' ),
        boolean$:  childrule( function(ctxt,p,v){return noval(v) || _.isBoolean(v)}, truefn, 'boolean$' ),
        date$:     childrule( function(ctxt,p,v){return noval(v) || _.isDate(v)}, truefn, 'date$' ),
        array$:    childrule( function(ctxt,p,v){return noval(v) || _.isArray(v)}, truefn, 'array$' ),
        object$:   childrule( function(ctxt,p,v){return noval(v) || _.isObject(v) && !_.isArray(v)}, truefn, 'object$' ),
        function$: childrule( function(ctxt,p,v){return noval(v) || _.isFunction(v)}, truefn, 'function$' ),
    
        lt$:  valrule( function(p,v){return p < v} ),
        lte$: valrule( function(p,v){return p <= v} ),
        gt$:  valrule( function(p,v){return p > v} ),
        gte$: valrule( function(p,v){return p >= v} ),
    
        min$: valrule( function(p,v){return p >= v}),
        max$: valrule( function(p,v){return p <= v}),
    
        uniq$: function(ctxt,cb){
          var value = ctxt.point
          var o = {}
    
          for( var i=0; i<value.length;i++ ) {
            if(_.has(o, value[i])){
              return ctxt.util.fail(ctxt,cb)
            } else {
              o[value[i]]= 1
            }
          }
    
          return cb()
        },
    
        only$: function(ctxt,cb) {
          var pn = ctxt.util.proplist(ctxt)
    
          for( var p in ctxt.point ) {
            if( !_.include(pn,p) ) {
              ctxt.prop = p
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          return cb();
        },
    
    
        wild$: function(ctxt,cb) {
          var value = ctxt.point
    
          if( !_.isUndefined(value) ) {
            if( !gex(ctxt.rule.spec).on(value) ) {
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          return cb();
        },
    
    
        eq$: function(ctxt,cb) {
          var value = ctxt.point
    
          if( !_.isUndefined(value) ) {
            if( ctxt.rule.spec !== value ) {
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          return cb();
        },
    
        minlen$: lenrule( function(valuelen, conditionlen){return valuelen >= conditionlen} ),
        maxlen$: lenrule( function(valuelen, conditionlen){return valuelen <= conditionlen} ),
    
        re$: function(ctxt,cb) {
          var value = ctxt.point
    
          if( !_.isUndefined(value) ) {
            value = ''+value
            var redef = ctxt.rule.spec
            var reopt = void(0)
    
            var m = /^\/(.*)\/(\w*)$/.exec(ctxt.rule.spec)
            if( m ) {
              redef = m[1]
              reopt = m[2]
            }
    
            var re = new RegExp(redef,reopt)
    
            if( !re.exec(value) ) {
              return ctxt.util.fail(ctxt,cb)
            }
          }
    
          return cb();
        },
    
    
        type$: function(ctxt,cb) {
          var pn = ctxt.util.proplist(ctxt)
    
          var checkmap = {
            string:_.isString,
            number:_.isNumber,
            integer:function(v){return _.isNumber(v) && v===(v|0)},
            boolean:_.isBoolean,
            date:_.isDate,
            array:_.isArray,
            object:function(v){return _.isObject(v) && !_.isArray(v) && !_.isDate(v)},
            'function':function(v){return _.isFunction(v)}
          }
    
          var found = 0;
          _.each(pn, function(p){
            var check = checkmap[p.toLowerCase()]
            if( check ) {
              found += check(ctxt.point)
            }
          })
    
          if( !found ) {
            return ctxt.util.fail(ctxt,cb)
          }
    
          return cb();
        },
    
        format$: function(ctxt,cb) {
          var pn = ctxt.util.proplist(ctxt)
    
          var checkmap = {
            datetime:    function checkFormatRegex(v) { return /\d{4}-(0[1-9]|1[1-2])-([0-2]\d|3[0-1])T([0-1]\d|2[0-4]):[0-5]\d:[0-5]\dZ/.test(v) },
            date:        function checkFormatRegex(v) { return /\d{4}-[0-1][0-2]-[0-2]\d/.test(v) },
            time:        function checkFormatRegex(v) { return /([0-1]\d|2[0-4]):[0-5]\d:[0-5]\dZ/.test(v) },
            utcmillisec: _.isNumber,
            re:          _.isRegExp
          }
    
          var found = 0;
          _.each(pn, function(p){
            var check = checkmap[p.toLowerCase()]
            if( check ) {
              found += check(ctxt.point)
            }
          })
    
          if( !found ) {
            return ctxt.util.fail(ctxt,cb)
          }
    
          return cb();
        },
    
        default$: function(ctxt,cb) {
          return cb();
        },
    
        enum$: function(ctxt,cb) {
          var value = ctxt.point
          var okvals = ctxt.rule.spec
    
          var avalue = []
    
          if ( _.isArray(value) ) {
            avalue = value
          }
          else {
            avalue[0] = value
          }
    
          var iserror = 0
          if( avalue ) {
            _.each(avalue, function(p){
              iserror += (-1 == okvals.indexOf(p) )
            })
          }
          if ( iserror ){
            return ctxt.util.fail(ctxt,cb)
          }
    
          return cb();
        },
  • ¶

    internal rules

    
        iterate$: function(ctxt,cb) {
          var pn = [ctxt.rule.spec.prop]
    
          if( _.isObject(ctxt.point) ) {
            if( _.isArray(ctxt.point) ) {
              pn = gex( ctxt.rule.spec.prop ).on( _.range(ctxt.point.length))
            }
            else {
              pn = _.keys(gex( ctxt.rule.spec.prop ).on(ctxt.point))
            }
          }
    
          var subctxt = ctxt.util.clone(ctxt)
          subctxt.rules = ctxt.rule.spec.rules
    
          function eachprop(propI) {
            if( propI < pn.length ) {
              var p = pn[propI]
    
              var psubctxt = ctxt.util.clone(subctxt)
    
              psubctxt.parents = subctxt.parents.concat({prop:p,point:subctxt.point})
    
              psubctxt.prop  = p
              psubctxt.point = subctxt.point ? subctxt.point[p] : null
    
              psubctxt.util.execrules(psubctxt,function(err){
                if( err ) return cb(err);
                eachprop(propI+1)
              })
            }
            else cb()
          }
          eachprop(0)
        },
    
    
        recurse$: function(ctxt,cb) {
    
          var recurctxt = ctxt.util.clone(ctxt)
          recurctxt.point = {$:ctxt.point}
          recurse('$',recurctxt.point,cb)
    
          function recurse(prop,point,cb) {
            if( !_.isObject(point) ) {
              return cb(null)
            }
    
            var pn = _.keys( point )
    
            var subctxt = ctxt.util.clone(ctxt)
            subctxt.rules   = ctxt.rule.spec.rules
            subctxt.parents = '$'!=prop?subctxt.parents.concat({prop:subctxt.prop,point:subctxt.point}):subctxt.parents
    
            function eachprop(propI,cb) {
              if( propI < pn.length ) {
                var p = pn[propI]
                var eachpropctxt = subctxt.util.clone(subctxt)
                eachpropctxt.prop  = p
                eachpropctxt.point = point[p]
                eachpropctxt.parents = '$'!=p?eachpropctxt.parents.concat({prop:subctxt.prop,point:subctxt.point}):eachpropctxt.parents
    
                eachpropctxt.util.execrules(eachpropctxt,function(err){
                  if( err ) return cb(err);
    
                  recurse(p,eachpropctxt.point,function(err){
                    if( err ) return cb(err);
    
                    eachprop(propI+1,cb)
                  })
                })
              }
              else {
                return cb(null)
              }
            }
            eachprop(0,cb)
          }
        },
    
    
        descend$: function(ctxt,cb) {
          var subctxt = ctxt.util.clone(ctxt)
          var prop = ctxt.rule.spec.prop
    
          subctxt.rules   = ctxt.rule.spec.rules
          subctxt.prop    = prop
          subctxt.point   = ctxt.point[prop]
          subctxt.parents = subctxt.parents.concat({prop:prop,point:ctxt.point})
    
          subctxt.util.execrules(subctxt,function(err){
            if( err ) return cb(err);
            cb(null)
          })
        }
      }
    
    
      var msgsmap = {
        no_input$:   "There is no input parameter",
    
        atmostone$:  "At most one of these properties can be used at a time: '<%=value%>'  (parent: <%=parentpath%>).",
        exactlyone$: "Exactly one of these properties must be used: '<%=value%>' (parent: <%=parentpath%>).",
        atleastone$: "At least one of these properties is required: '<%=value%>' (parent: <%=parentpath%>).",
    
        required$:   "The property '<%=property%>' is missing and is always required (parent: <%=parentpath%>).",
        notempty$:   "The property '<%=property%>' requires a value (parent: <%=parentpath%>).",
    
        string$:     "The property '<%=property%>', with current value: '<%=value%>', must be a string (parent: <%=parentpath%>).",
        integer$:    "The property '<%=property%>', with current value: '<%=value%>', must be a integer (parent: <%=parentpath%>).",
        number$:     "The property '<%=property%>', with current value: '<%=value%>', must be a number (parent: <%=parentpath%>).",
        boolean$:    "The property '<%=property%>', with current value: '<%=value%>', must be a boolean (parent: <%=parentpath%>).",
        date$:       "The property '<%=property%>', with current value: '<%=value%>', must be a date (parent: <%=parentpath%>).",
        array$:      "The property '<%=property%>', with current value: '<%=value%>', must be a array (parent: <%=parentpath%>).",
        object$:     "The property '<%=property%>', with current value: '<%=value%>', must be a object (parent: <%=parentpath%>).",
        function$:   "The property '<%=property%>', with current value: '<%=value%>', must be a function (parent: <%=parentpath%>).",
    
        only$:       "The property '<%=property%>' is not recognised here. Recognised properties are: <%=rule.spec%> (parent: <%=parentpath%>).",
    
        wild$:       "The value <%=value%> does not match the expression '<%=rule.spec%>' (parent: <%=parentpath%>).",
        re$:         "The value <%=value%> does not match the regular expression <%=rule.spec%> (parent: <%=parentpath%>).",
        type$:       "The value <%=value%> is not of type '<%=rule.spec%>' (parent: <%=parentpath%>).",
        format$:     "The value <%=value%> is not of format '<%=rule.spec%>' (parent: <%=parentpath%>).",
    
        minlen$:     "The property '<%=property%>', with current value: '<%=value%>', must have minimum length '<%=rule.spec%>' (parent: <%=parentpath%>).",
        maxlen$:     "The property '<%=property%>', with current value: '<%=value%>', must have maximum length '<%=rule.spec%>' (parent: <%=parentpath%>).",
    
        eq$:         "The value <%=value%> does not equal '<%=rule.spec%>' (parent: <%=parentpath%>).",
        lt$:         "The value <%=value%> is not less than '<%=rule.spec%>' (parent: <%=parentpath%>).",
        lte$:        "The value <%=value%> is not less than or equal with '<%=rule.spec%>' (parent: <%=parentpath%>).",
        gt$:         "The value <%=value%> is not greater than '<%=rule.spec%>' (parent: <%=parentpath%>).",
        gte$:        "The value <%=value%> is not not greater than or equal with '<%=rule.spec%>' (parent: <%=parentpath%>).",
        min$:        "The value <%=value%> is not not greater than or equal with '<%=rule.spec%>' (parent: <%=parentpath%>).",
        max$:        "The value <%=value%> is not less than or equal with '<%=rule.spec%>' (parent: <%=parentpath%>).",
        uniq$:       "The value <%=value%> has duplicate elements.",
        enum$:       "The value <%=value%> must be one of '<%=rule.spec%>' (parent: <%=parentpath%>)."
    
      }
    
    
    
    
      function clone(ctxt) {
        var newctxt = {
          rules:ctxt.rules,
          point:ctxt.point,
          msgs:ctxt.msgs,
          log:ctxt.log,
          parents:ctxt.parents,
          util:ctxt.util
        }
        return newctxt;
      }
    
    
      function formatparents(parents,topname) {
        var out = topname || 'top level'
        if( 0 < parents.length ) {
          out = _.map(parents,function(p){return p.prop}).join('.')
          if( topname ) {
            out = topname+'.'+out
          }
        }
    
        return out
      }
    
    
      function killcircles() {
        var seen = []
        return function(k,v){
          if( _.contains(seen,v) ) return '[CIRCULAR-REFERENCE]';
          seen.push(v)
          return v
        }
      }
    
    
    
      function fail() {
        var code = arguments[0]
        var ctxt = arguments[1]
        var cb   = arguments[2]
    
        if( !cb ) {
          ctxt = arguments[0]
          cb   = arguments[1]
          code = ctxt.rule.name
        }
    
        if(!cb) {
          throw new Error('Parambulator: ctxt.util.fail: callback undefined')
        }
    
        if(!ctxt) {
          return cb(new Error('Parambulator: ctxt.util.fail: ctxt undefined'))
        }
    
        var inserts = {
          property:ctxt.prop,
          value:ctxt.value||JSON.stringify(ctxt.point,killcircles()),
          point:ctxt.point,
          rule:ctxt.rule,
          parentpath:ctxt.util.formatparents(ctxt.parents),
          json:function(v){return JSON.stringify(v,killcircles())}
        }
    
        var msg = ctxt.msgs[code] || code
    
        if( _.isFunction(msg) ) {
          msg = msg(inserts,ctxt)
        }
        else {
          msg = ctxt.util.msgmods( msg )
          msg = _.template(msg,inserts)
        }
    
        var err = new Error( msg )
    
        err.parambulator = {
          code:     code,
          property: ctxt.prop,
          value:    ctxt.point,
          expected: (ctxt.rule ? ctxt.rule.spec : void 0),
          parents:  ctxt.parents,
          point:    ctxt.point,
          rule:     ctxt.rule}
        return cb(err)
      }
  • ¶

    Return an ordered array of property names. The prefix __ is removed from property names, both in the returned array, and the original object.

      function proporder(obj) {
        var pn = []
        for( var p in obj ) {
          var pc = p
          if( 0 === p.indexOf('__') ) {
            pc = p.substring(2)
            obj[pc] = obj[p]
            delete obj[p]
          }
          pn.push(pc)
        }
        return pn
      }
    
    
      /*
    
       name$ -> rule name
       name  -> property names
    
       */
      function Parambulator( spec, pref ) {
        var self = {}
        pref = pref || {}
        var defaultrules = []
    
        if( _.isString(spec) ) {
          spec = jsonic(spec)
        }
    
        if( !spec || !_.isObject(spec) || _.isArray(spec) ) {
          throw new Error('Parambulator: spec argument is not an object')
        }
    
        /*
         if( exp.ownparams ) {
         exp.ownparams.validate(spec,function(err){
         if( err ) throw err;
         })
         }
         */
    
        if( pref ) {
          if( exp.ownprefs ) {
            exp.ownprefs.validate(pref,function(err){
              if( err ) throw err;
            })
          }
    
          if( pref.valid && exp ) {
            var prefparams = exp({'**':pref.valid})
            prefparams.validate(spec,function(err){
              if( err ) throw err;
            })
          }
        }
  • ¶

    var rulenames = proporder(spec)

        var rules = parse(spec)
        parsedefault(spec, [], [])
    
    
        self.toString = function() {
          return JSON.stringify(rules)
        }
    
    
        /*
         * Example:
         * For a: {default$:123, type$:'number'}
         * creates {"pathnames":["a"],"pathtypes":[],"defaultvalue":123}
         *
         * for d: {type$: 'array', __0: {default$:'arraytest0'}}
         * creates {"pathnames":["d","0"],"pathtypes":["array"],"defaultvalue":"arraytest0"}
         */
        function parsedefault(spec, path, pathtypes){
          var innerulenames = []
          var currentruletype = 'object'
    
          for(var name in spec){
            if(spec.hasOwnProperty(name)) {
              var rule = spec[name]
              if ('default$' == name){
                var defaultrule = {}
                defaultrule.pathnames = path
    
                defaultrule.pathtypes = pathtypes.splice(1,pathtypes.length)
                defaultrule.defaultvalue = rule
                defaultrules.push(defaultrule)
              }
              if ('type$' == name){
                currentruletype = rule
              }
              if( _.isObject(rule) && !_.isArray(rule) ) {
                innerulenames.push(name)
              }
            }
          }
    
          for (var index in innerulenames){
            if(innerulenames.hasOwnProperty(index)) {
              var inner_rule = spec[innerulenames[index]]
    
              var newpath = path.slice()
              newpath.push(innerulenames[index])
    
              var newpathtypes = pathtypes.slice()
              newpathtypes.push(currentruletype)
              parsedefault(inner_rule, newpath, newpathtypes)
            }
          }
        }
    
    
        function buildrule(name,rulespec) {
          var func = (pref && pref.rules) ? pref.rules[name] : null
          if( !func ) {
            func = rulemap[name]
          }
    
          if( func ) {
            var rule = {
              func:func,
              name:name,
              spec:rulespec
            }
            return rule
          }
          else {
            throw new Error("Parambulator: Unknown rule: "+name)
          }
        }
    
    
        function parse(spec) {
          var rules = []
          var names = proporder(spec)
          _.each(names, function(name){
            var rulespec = spec[name]
  • ¶

    enables multiple rules of same name

            if( 'list$' == name ) {
              for(var i = 0; i < rulespec.length; i++) {
                var rs = {}
                rs[rulespec[i][0]]=rulespec[i][1]
                rules.push(parse(rs)[0])
              }
            }
  • ¶

    enables quoting of property names that end in $

            else if( 'prop$' == name ) {
              var subrules = parse( rulespec.rules )
              var rule = buildrule('descend$',{prop:rulespec.name,rules:subrules})
              rules.push(rule)
            }
  • ¶

    it’s a rule - name$ syntax

            else if( name.match(/\$$/) ) {
              if((name === 'required$' || name === 'notempty$') && 
                 pref.multiErrors && 
                 _.isArray(rulespec)) 
              {
                _.each(rulespec, function(item) {
                  var item_rule = buildrule(name,[item],spec)
                  rules.push(item_rule)
                })
              } 
              else {
                var build_rule = buildrule(name,rulespec,spec)
                rules.push(build_rule)
              }
            }
    
    
            else if( '**' == name ) {
              var starstar_subrules = parse( rulespec )
              var starstar_rule = 
                    buildrule('recurse$',{prop:name,rules:starstar_subrules})
              rules.push(starstar_rule)
            }
  • ¶

    it’s a property

            else {
              if( _.isObject(rulespec) && !_.isArray(rulespec) ) {
    
                _.each( rulespec, function(v,p){
                  if( p.match(/\$$/) && _.isBoolean(v) && v ) {
                    var rule = buildrule(p,name,spec)
                    rules.push(rule)
                    delete rulespec[p]
                  }
                })
    
                var prop_subrules = parse( rulespec )
                var prop_rule = buildrule('iterate$',{prop:name,rules:prop_subrules})
                rules.push(prop_rule)
              }
    
    
              else if( _.isString(rulespec) ) {
  • ¶

    foo:’required$’

                if( rulespec.match(/\$/) ) {
                  var rulespecs = rulespec.split(/\s*,\s*/)
                  _.each( rulespecs, function( rulespec ) {
                    rules.push( buildrule(rulespec,name) )
                  })
                }
  • ¶

    foo:’bar*’

                else {
                  rules.push( buildrule('descend$',{
                    prop:name,rules:[buildrule('wild$',rulespec)]
                  }))
                }
              }
  • ¶

    TODO: else check for other types, and use eq$ !!!

              else if( _.isNumber(rulespec) ) {
                rules.push( buildrule('descend$',{
                  prop:name,rules:[buildrule('eq$',rulespec)]
                }))
              }
    
              else if( _.isBoolean(rulespec) ) {
                rules.push( buildrule('descend$',{
                  prop:name,rules:[buildrule('eq$',rulespec)]
                }))
              }
    
              else {
                throw new Error("Parambulator: Unrecognized rule specification: "+rulespec)
              }
            }
    
          })
    
          return rules
        }
    
    
        var msgs = _.extend({},msgsmap,pref?pref.msgs:null)
    
    
        self.validate = function( args, cb ) {
          var reterr
          var callback = pref.callbackmaker ? pref.callbackmaker(cb) : (cb||function(){})
          var wrapcb = function(err){
            reterr=err
  • ¶

    console.log(‘VALIDATE’, JSON.stringify(arguments), arrayify(arguments))

            callback.apply(null,arguments)
          }
          var errors = []
    
    
          function execrules(ctxt,cb) {
            if( _.isUndefined(args) ) {
              return fail('no_input$',ctxt,cb)
            }
    
            var rules = ctxt.rules
    
            function execrule(ruleI) {
              if( ruleI < rules.length ) {
                var rule = rules[ruleI]
    
                if( !ctxt.point ) {
                  return execrule(ruleI+1)
                }
    
                ctxt.rule = rule
    
                var specstr = JSON.stringify(rule.spec,killcircles())
    
                ctxt.log.push('rule:'+rule.name+':exec:'+specstr)
    
                rule.func(ctxt, function(err) {
                  if( err ) {
                    if(_.isArray(err)) {
  • ¶

    huh !? errors = errors.concat(err)

                    } else {
                      errors.push(err)
                      ctxt.log.push('rule:'+rule.name+':fail:'+specstr)
                    }
                    execrule(ruleI+1)
                  }
                  else {
                    ctxt.log.push('rule:'+rule.name+':pass:'+specstr)
                    execrule(ruleI+1)
                  }
                })
              }
              else {
                if(errors.length > 0 && pref.multiErrors) {
                  cb(errors,{log:ctxt.log})
                } else {
                  var err = errors.length > 0 ? errors[0] : null;
                  cb(err,{log:ctxt.log})
                }
    
              }
            }
    
            execrule(0)
          }
    
          /*
           * Example:
           * For a: {"pathnames":["a"],"pathtypes":[],"defaultvalue":123}
           * creates {a:123}
           *
           * for d: {"pathnames":["d","0"],"pathtypes":["array"],"defaultvalue":"arraytest0"}
           * creates {d: ['arraytest0']}
           */
          function validatedefaults(ctxt, cb){
            for (var ruleindex in defaultrules){
              var rule = defaultrules[ruleindex]
              var obj = ctxt.point
    
              for (var index in rule.pathnames){
                var location = rule.pathnames[index]
                if ( !_.has(obj, location) ){
  • ¶

    if is last in path then just add default value

                  if (index == rule.pathnames.length - 1){
                    obj[location] = rule.defaultvalue
                  }
  • ¶

    else create object or array and continue following path

                  else{
                    var type = rule.pathtypes[index]
                    var newobj
                    if ('object' == type){
                      newobj = {}
                    }
                    else if ('array' == type){
                      newobj = []
                    }
                    else{
  • ¶

    cannot continue, call cb and return false;

                      ctxt.util.fail('default$',ctxt,cb)
                      return;
                    }
                    obj[location] = newobj
                    obj = newobj
                  }
                }
                else{
                  obj = obj[location]
                }
              }
            }
            cb();
          }
    
          var ctxt = {rules:rules,point:args,msgs:msgs,log:[],parents:[]}
          ctxt.util = {
            formatparents:function(){
              var args = arrayify(arguments)
              args[1] = pref && pref.topname && !args[1] ? pref.topname : args[1]
              return formatparents.apply(null,args)
            },
            msgmods:function(msg) {
              return (pref.msgprefix||'') + msg + (pref.msgsuffix||'')
            },
            fail:fail,proplist:proplist,execrules:execrules,clone:clone}
    
          validatedefaults(ctxt,function(err){
            if (err){
              wrapcb(err,{log:ctxt.log})
            }
            else {
              execrules(ctxt,function(err){
                wrapcb(err,{log:ctxt.log})
              })
            }
          })
  • ¶

    only works if no async calls inside rules

          return reterr
        }
    
    
        return self
      }
    
    
      var exp = function(){
        var args = arrayify(arguments)
        return Parambulator.apply(this,args)
      }
  • ¶

    this is where Parambulator validates it’s own input

      exp.ownparams = new Parambulator({
        '**':
        {
          strings$: ['required$','notempty$','atmostone$','exactlyone$','atleastone$'],
          list$:[
            ['prop$',{name:'wild$', rules:{type$:'string'}}],
            ['prop$',{name:'type$', rules:{type$:['string','array']}}],
            ['prop$',{name:'format$', rules:{type$:['string','array']}}],
            ['prop$',{name:'re$',   rules:{type$:'string'}}],
    
            ['prop$',{name:'type$', rules:{enum$:['string','number','integer','boolean','date','array','object',]}}],
            ['prop$',{name:'format$', rules:{enum$:['datetime','date','time','utcmillisec', 're']}}],
    
            ['prop$',{name:'minlen$', rules:{type$:'number'}}],
            ['prop$',{name:'maxlen$', rules:{type$:'number'}}],
    
            ['prop$',{name:'enum$', rules:{type$:'array'}}],
            ['prop$',{name:'list$', rules:{type$:'array'}}],
  • ¶

    This can work only after #1 is implemented these should also work for strings, right? [‘prop$’,{name:’lt$’, rules:{type$:[‘number’,’date’]}}], [‘prop$’,{name:’lte$’, rules:{type$:[‘number’,’date’]}}], [‘prop$’,{name:’gt$’, rules:{type$:[‘number’,’date’]}}], [‘prop$’,{name:’gte$’, rules:{type$:[‘number’,’date’]}}], [‘prop$’,{name:’min$’, rules:{type$:[‘number’,’date’]}}], [‘prop$’,{name:’max$’, rules:{type$:[‘number’,’date’]}}],

            ['prop$',{name:'uniq$', rules:{type$:'array'}}]
          ]
        }
      }, {
        __ownparams__:true,
        rules: {
          strings$: function(ctxt,cb){
            var pn = ctxt.rule.spec
    
            if( !_.isArray(pn) ) {
              pn = [''+pn]
            }
    
            for( var pI = 0; pI < pn.length; pI++ ) {
              var p   = pn[pI]
              var val = ctxt.point[p]
    
              if( !_.isUndefined(val) ) {
                if( _.isString(val) ) {
  • ¶

    ok

                }
                else if( _.isArray(val) ) {
                  for(var i = 0; i < val.length; i++ ) {
                    if( !_.isString(val[i]) ) {
                      ctxt.prop = p
                      return ctxt.util.fail(ctxt,cb)
                    }
                  }
                }
                else {
                  ctxt.prop = p
                  return ctxt.util.fail(ctxt,cb)
                }
              }
            }
    
            cb(null)
          }
        },
        msgs: {
          'strings$': 'The <%=property%> rule needs a string or array of strings (property: <%=parentpath%>).'
        },
        topname:'spec'
      })
  • ¶

    validate preferences

      exp.ownprefs = new Parambulator({
        object$:['valid','rules','msgs'],
        string$:['topname','msgprefix','msgsuffix'],
        boolean$:['multiErrors'],
        function$:['callbackmaker'],
        only$:['valid','rules','msgs', 'topname','msgprefix','msgsuffix', 'multiErrors', 'callbackmaker']
      },{
        topname:'prefs'
      })
    
      exp.Parambulator = Parambulator
  • ¶

    module.exports = exp

    
    
      root.parambulator = exp
    
      exp.noConflict = function() {
        root.previous_parambulator = previous_parambulator;
        return self;
      }
    
    
      if( typeof exports !== 'undefined' ) {
        if( typeof module !== 'undefined' && module.exports ) {
          exports = module.exports = exp
        }
        exports.parambulator = exp
      }
      else {
        root.parambulator = exp
      }
    
    }).call(this);