Logan Campbell

I write about building web apps using FP. Mostly Clojure and ClojureScript.

What to look for in validations

25 Nov 2013

Validations tell us that some data is valid.

(valid? 1) => true

And some data is invalid.

(valid? 2) => false

But not all data is valid in all circumstances. So we need more than one function.

(even? 2) => true

(neg? 2) => false

We should be able to define our own validations.

(silly? :clown-shoes) => true

And have functions that construct validations.

(def my-validation
    (after #inst "2013-12-01"))

(my-validation #inst "2013-12-02") => true

To check more than one rule at a time we need a way of composing validations.

(def during-summer?
  (join (after  #inst "2013-12-01")
        (before #inst "2014-03-01")))

The composed validations should be composeable as well, so that they may be re-used.

(def good-day-for-a-picnic?
  (join during-summer?

(good-day-for-a-picnic? #inst "2013-04-14")
  => false

Now we know our data is bad, but not why. We need more information.

(good-day-for-a-picnic? #inst "2013-04-14")
  => "That's not during summer"

Of course data can be wrong in more than one way.

(good-day-for-a-picnic? #inst "2013-04-15")
  => ["That's not during summer"
      "Mondays suck for picnics"]

Though sometimes the reasons are redundant.

(def strong-password?
  (join not-empty?
        (matches? #"[a-z]")
        (matches? #"[A-Z]")
        (matches? #"[0-9]")))

(strong-password? "")
  => ["You didn't give a password"
      "The password you didn't give has no lower case letters"
      "Or any upper case letters for that matter"
      "Unsurprisingly it was missing numbers too"]

We should be able to say when a branch of validation should fail fast.

(def strong-password?
  (chain not-empty?
         (join (matches? #"[a-z]")
               (matches? #"[A-Z]")
               (matches? #"[0-9]"))))

(strong-password? "")
  => ["You didn't give a password"]

But error messages are useless if you can't read them.

(def errors
  (strong-password? "kiTTenS"))

(english errors)
  => ["Your password must contain numbers"]

(russian errors)
  => ["Вы должны быть пьян"]

Even computers should be able to understand them.

  => [{:type    :matches
       :pattern #"[0-9]"}]

(track-common-errors! errors)
  => {:contains   22
      :not-empty? 400}

And I think it goes without saying that your validations should be able to validate any type of data.

(prime? 3)
  => []

(not-empty? "")
  => [{:type :not-empty?}]

((length-eq? 3) [1 2 3])
  => []

((has-keys? :name :age) {:name "Logan"})
  => [{:type :has-keys? :key :age}]

And they shouldn't be coupled to a web framework, or a database layer or a CSV importer.

If you agree with me then try Vlad.

comments powered by Disqus