Clojure validation in 30 lines
Tags: clojure, validation
Published: 2020-10-20 09:46:55

_A function composition approach to validation._

In the previous post, I offered some critique of the validation approaches I found. In his post I bash together an alternative solution.

# Goals

Here are the things I was interested in.

* validation definitions are functions
* composable from smaller pieces
* no macros
* simple errors

# 5 functions

There are 5 core functions to compose validations.

## `fn`

This creates a basic validation.

```clojure
(require '[validation.core :as v])

(def validate
  (v/fn :required not-empty))

(validate "asdf")
=> "asdf"

(validate "")
=> {:v/error :required}

(v/error (validate ""))
=> :required
```

Any function can be used for validation provided it returns nil if anything goes wrong. When that happens, `:v/error` is returned with the provided trait (`:required` here).

> You provide a trait so you know exactly the errors to expect.

## `and`

This combinator can be used to send a value through a series of transformations, each of which might fail. The end value will be returned if all steps are successful. Otherwise the failing trait will be returned as an error.

```clojure
(def validate
  (v/and (v/fn :required not-empty)
         (v/fn :keyword keyword)))

(validate "asdf")
=> :asdf

(validate "")
=> {:v/error :required}
```

> Note: This is not a strong validation because it does not deal well with non-string inputs or whitespace.

## `seq`, `hmap`

Validate a sequence.

```clojure
(def validate
  (v/seq
   (v/and (v/fn :required not-empty)
          (v/fn :keyword keyword))))

(validate ["a" "b"])
=> (:a :b)

(validate ["a" ""])
=> (:a {:v/error :required})
```

Validate a hash-map.

```clojure
(def validate
  (v/hmap
   {:a (v/fn :required not-empty)
    :b (v/and (v/fn :required not-empty)
              (v/fn :keyword keyword))}))

(validate {:a "a" :b "b"})
=> {:a "a", :b :b}

(validate {:a "a" :b ""})
=> {:a "a", :b {:v/error :required}}

(map :b [{:a "a" :b ""}
         (validate {:a "a" :b ""})])
=> ("" {:v/error :required})
```

## `or`

This combinator returns the first passing validation.

```clojure
(def validate
  (v/or :date-or-int
        (v/fn :date str->date)
        (v/fn :int str->int)))
```

It takes an additional trait since returning every failing trait would not would not be entirely useful.

# Validation functions

The 30 lines of code are just the definition of the 5 core functions and 2 error checks. Validation functions are still needed to plug into them. Clojure has some built-in functions that can be used but you are probably going to want more. Fortunately they are easy to create. Here are some examples.

```clojure
(defn non-blank [s]
  (when (string? s)
    (not-empty (trim s))))

(defn in-range [min-val max-val]
  (fn [v]
    (when (and (>= v min-val)
               (<= v max-val))
      v)))
```

When a validation takes multiple parameters as `in-range` does above, it should return a function to take the last parameter. This bakes in the necessary partial application.

```clojure
(def validate
  (v/fn :range (in-range 0 4)))

(validate 3)
=> 3

(validate 5)
=> {:v/error :range}
```

# Conclusion

I really like the function composition approach to validation. It is composable and extensible. Its implementation is very small (see below). Which means I can easily change or add more functions. I find it works quite well for user input validation.

----

### The 30 lines

```clojure
(ns validation.core
  (:refer-clojure :exclude [and or fn seq]))

(defn fn [trait f]
  #(if-some [value (f %)]
    value
    {::error trait}))

(defn and [f & fs]
  #(loop [v (f %) fs fs]
     (if (contains? v ::error)
       v
       (let [f (first fs)]
         (if (nil? f)
           v
           (recur (f v) (next fs)))))))

(defn or [trait f & fs]
  #(loop [possible (f %) fs fs]
     (if-not (contains? possible ::error)
       possible
       (let [f (first fs)]
         (if (nil? f)
           {::error trait}
           (recur (f %) (next fs)))))))

(defn hmap [m]
  #(reduce-kv update % m))

(defn seq [f]
  #(map f %))

(defn error? [v]
  (contains? v ::error))

(defn error [v]
  (::error v))
```