String validation

Googie I playied a little with jquery and its string validation feature lately and I really liked it. I also neeeded server-side validation for values provided in a form, so I wrote something similar.

Few words about how to use it. First argument is a list with even number of elements. They are used as pairs: {fieldName ruleList}. The fieldName is actually variable name from current context. RuleList is also a list. Supported rules are described in sourcecode header. Some rules require parameters - in these cases rule should be specified as a list itself, where first argument is the rule and rest elements are parameters to the rule.

This might seem complicated, but there's an example below the sourcecode, so I think you will find it very simple and cool to use ;) I do so.

Second argument to the procedure is similar mapping for messages in case some rule is not satisfied. If message(s) is not specified, then default one is used. Not much to explain here - see example below.

Here's code:

#
# Rules: integer, boolean, double, email, url, glob, regexp, required, minlength, maxlength, equalTo, function
#
proc validate {varAndRules {messages ""}} {
    set details [dict create]
    set result [dict create valid true numberOfErrors 0]
    foreach {varName rules} $varAndRules {
        upvar $varName local_$varName
        if {![info exists local_$varName]} {
            set value ""
        } else {
            set value [set local_$varName]
        }

        set fieldDetails [dict create valid true errors [list]]
        
        if {[dict exists $messages $varName]} {
            set fieldMessages [dict get $messages $varName]
        } else {
            set fieldMessages [dict create]
        }

        foreach rule $rules {
            set ruleType [lindex $rule 0]
            if {[dict exists $fieldMessages $ruleType]} {
                set fieldMessage [dict get $fieldMessages $ruleType]
            } else {
                set fieldMessage ""
            }

            set valid true
            set msg ""
            switch -- $ruleType {
                "required" {
                    if {$value == ""} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' is required."}]
                    }
                }
                "notEmpty" {
                    if {[string trim $value] == ""} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' cannot be empty."}]
                    }
                }
                "integer" {
                    if {$value == "" || ![string is integer $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' has to be an integer."}]
                    }
                }
                "boolean" {
                    if {$value == "" || ![string is boolean $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' has to be a boolean."}]
                    }
                }
                "double" {
                    if {$value == "" || ![string is double $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' has to be an a double."}]
                    }
                }
                "email" {
                    if {![regexp -- {^[A-Z0-9._%+-]+\@[A-Z0-9.-]+\.[A-Z]{2,4}$} $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' has to be valid e-mail address."}]
                    }
                }
                "glob" {
                    set ruleArg [lindex $rule 1]
                    if {![string match $ruleArg $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $ruleArg] :
                            "'$varName' has to match '$ruleArg' mask."}]
                    }
                }
                "regexp" {
                    set ruleArg [lindex $rule 1]
                    if {![regexp -- $ruleArg $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $ruleArg] :
                            "'$varName' has to match '$ruleArg' regular expression."}]
                    }
                }
                "url" {
                    if {![regexp -- {^((http|https|ftp):\/\/)?(www\.)?([a-z0-9\-\.]{4,})(\/)?.*$} $value]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName] :
                            "'$varName' has to be valid URL."}]
                    }
                }
                "minlength" {
                    set ruleArg [lindex $rule 1]
                    if {[string length $value] < $ruleArg} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $ruleArg] :
                            "'$varName' has to be at least $ruleArg characters length."}]
                    }
                }
                "maxlength" {
                    set ruleArg [lindex $rule 1]
                    if {[string length $value] > $ruleArg} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $ruleArg] :
                            "'$varName' has to be $ruleArg characters length at most."}]
                    }
                }
                "equalTo" {
                    set ruleArg [lindex $rule 1]
                    set secondValue [uplevel [list set $ruleArg]]
                    if {![string equal $value $secondValue]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $ruleArg] :
                            "'$varName' has to be equal to '$ruleArg'."}]
                    }
                }
                "function" {
                    set ruleArg [lindex $rule 1]
                    set fnName [lindex $ruleArg 0]
                    set call $fnName
                    foreach fnArg [lrange $ruleArg 1 end] {
                        if {[string match "\$*" $fnArg]} {
                            lappend call [uplevel [list set [string range $fnArg 1 end]]]
                        } else {
                            lappend call $fnArg
                        }
                    }
                    if {![uplevel #0 $call]} {
                        set valid false
                        set msg [expr {$fieldMessage != "" ?
                            [format $fieldMessage $varName $fnName] :
                            "'$varName' doesn't satisfy requirements of '$fnName' function."}]
                    }
                }
            }
            if {!$valid} {
                dict set fieldDetails valid false
                dict lappend fieldDetails errors $msg
                dict set result valid false
                dict incr result numberOfErrors
            }
        }
        dict set details $varName $fieldDetails
    }
    dict set result details $details
    return $result
}

Example of usage:

proc checkIfLoginAvailable {u} {
    expr {$u == "taken_name"}
}

set name "aa"
set email "bb"
set pass1 "123"
set pass2 "545"
set username "test123"

set res [validate {
    name {
        required
    }
    email {
        required
        email
    }
    pass1 {
        required
    }
    pass2 {
        required
        {equalTo pass1}
    }
    username {
        notEmpty
        {function {checkIfLoginAvailable $username}}
    }
} {
    name {
        required {My custom message about field %s.}
    }
    pass2 {
        equalTo {Password mismatch.}
    }
}]

puts "errors: [dict get $res numberOfErrors]"
dict for {key val} [dict get $res details] {
    puts "$key: $val"
}

Result would be:

errors: 3
name: valid true errors {}
email: valid false errors {{'email' has to be valid e-mail address.}}
pass1: valid true errors {}
pass2: valid false errors {{Password mismatch.}}
username: valid false errors {{'username' doesn't satisfy requirements of 'checkIfLoginAvailable' function.}}