Skip to content

RyanMcG/manners

Repository files navigation

manners Build Status

A validation library built on using predicates properly.

;; Add the following to dependencies in your project.clj
[manners "0.8.0"]

Thoughts

Composing predicates is a very easy thing to do in Clojure and I wanted a library which takes advantage of that without too much magic.

Usage

Terminology

First some terms vital to manner's lexicon.

  • etiquette - A sequence of manners
  • manner - One or more coaches or predicate message pairs
  • coach - A function that takes a value and returns a sequence (preferably lazy) of messages. Coach functions (not vars) must have the meta ^:coach. They may be created using as-coach which also does some memoization.
  • message - A message could be anything but is most often a string describing what makes the given value invalid.

Creating coaches

manner

There are several functions which create coaches. The most essential is manner which creates a coach from a manner (see terminology above) like so:

(def div-by-six-coach (manner even? "must be even"
                              #(zero? (mod % 6)) "must be divisible by 6"))
(div-by-six-coach 1)  ; → ("must be even")
(div-by-six-coach 2)  ; → ("must be divisible by 6")
(div-by-six-coach 12) ; → ()

manner is an idempotent function.

(def div-by-six-coach2 (manner (manner (manner (manner div-by-six-coach)))))
;; The behaviour of div-by-six-coach and div-by-six-coach2 is the same
(div-by-six-coach2 2) ; → ("must be divisible by 6")

manners

manners creates a coach from a one or more manners or coaches. Instead of returning the first matching message it returns the results of every coach.

(def div-by-six-and-gt-19-coach
  (manners div-by-six-coach
           [#(>= % 19) "must be greater than or equal to 19"]))

(div-by-six-and-gt-19-coach 1)
; → ("must be even" "must be greater than or equal to 19")
(div-by-six-and-gt-19-coach 2)
; → ("must be divisible by 6" "must be greater than or equal to 19")
(div-by-six-and-gt-19-coach 12)
; → ("must be greater than or equal to 19")
(div-by-six-and-gt-19-coach 24) ; → ()

manners is also idempotent.

(def div-by-six-coach2 (manners (manner (manners (manners div-by-six-coach)))))
;; The behaviour of div-by-six-coach and div-by-six-coach2 is the same
(div-by-six-coach2 2) ; → ("must be divisible by 6")

etiquette

This function is almost identical to manners. Actually, manners is defined using etiquette.

(defn manners [& etq]
  (etiquette etq))

Instead of passing in an arbitrary number of arguments, etiquette is a unary function which takes all manners as a sequence.

;; These are all the same...
(etiquette [[div-by-six-coach]
            [#(>= % 19) "must be greater than or equal to 19"]])
(etiquette [div-by-six-coach
            [#(>= % 19) "must be greater than or equal to 19"]])
(manners [div-by-six-coach]
         [#(>= % 19) "must be greater than or equal to 19"])
(manners div-by-six-coach
         [#(>= % 19) "must be greater than or equal to 19"])

And finally, etiquette is an idempotent function too.

Helpers

bad-manners & coach

(use 'manners.victorian)
(def etq [[even? "must be even"
           #(zero? (mod % 6)) "must be divisible by 6"]
          [#(>= % 19) "must be greater than or equal to 19"]])
(bad-manners etq 11)
; → ("must be even" "must be greater than or equal to 19")
(bad-manners etq 10) ; → ("must be greater than or equal to 19")
(bad-manners etq 19) ; → ("must be even")
(bad-manners etq 20) ; → ("must be divisible by 6")
(bad-manners etq 24) ; → ()

bad-manners is simply defined as:

(defn bad-manners
  [etq value]
  ((etiquette etq) value))

Memoization is used so that subsequent calls to coach and the function generated by coach does not repeat any work. That also means predicates used in an etiquette should be referentially transparent.

proper? & rude?

Next are proper? and rude?. They are complements of each other.

;; continuing with the etiquette defined above.
(proper? etq 19) ; → false
(proper? etq 24) ; → true
(rude? etq 19)   ; → true
(rude? etq 24)   ; → false

proper? is defined by calling empty? on the result of bad-manners. With the memoization you can call proper? then check bad-manners without doubling the work.

(if (proper? etq some-value)
  (success-func)
  (failure-func (bad-manners etq some-value))

Of course we all want to be dry so you could do the same as above with a bit more work that does not rely on the memoization. Pick your poison.

(let [bad-stuff (bad-manners etq some-value)]
  (if (empty? bad-stuff)
    (success-func)
    (failure-func bad-stuff)))

avow & falter

Next on the list is avow which takes the results of a call to bad-manners and throws an AssertionError when a non-empty sequence is returned. avow is conceptionally like the composition of falter (which does the throwing) and a coach.

;; assume `etq` is a valid etiquette and `value` is the value have
;; predicates applied to.
((comp falter (etiquette etq)) value)

An example:

(avow [[identity "must be truthy"]] nil)
; throws an AssertionError with the message:
;   Invalid: must be truthy

defmannerisms

The last part of the API is defmannerisms. This is a helper macro for defining functions that wrap the core API and a given etiquette.

(defmannerisms empty-coll
  [[identity "must be truthy"
    coll? "must be a collection"]
   [empty? "must be empty"]])

(proper-empty-coll? [])      ; → true
(rude-empty-coll? [])        ; → false
(bad-empty-coll-manners nil) ; → ("must be truthy")
(bad-empty-coll-manners "")  ; → ("must be a collection")
(bad-empty-coll-manners "a") ; → ("must be a collection" "must be empty")
(avow-empty-coll 1)
; throws an AssertionError with the message:
;   Invalid empty-coll: must be truthy

;; And so on.

Composability

Since etiquettes and manners can contain coaches, coaches can be composed of other coaches. In fact, when manners is processing an etiquette it transforms predicate message pairs into coaches as a first step.

(def my-map-coach
  (etiquette [[:a "must have key a"
               (comp number? :a) "value at a must be a number"]
              [:b "must have key b"]]))
;; Just for reference
(my-map-coach {})        ; → ("must have key a" "must have key b")
(my-map-coach {:a 1})    ; → ("must have key b")
(my-map-coach {:a true}) ; → ("value at a must be a number" "must have key b")
(my-map-coach {:b 1})    ; → ("must have key a")

;; We can copy a coach by making it the only coach in a one manner etiquette.
(def same-map-coach (etiquette [[my-map-coach]]))
;; Or with manners
(def same-map-coach (manners [my-map-coach]))
(def same-map-coach (manners my-map-coach)) ;; This works too
;; Or with manner
(def same-map-coach (manner my-map-coach))
(same-map-coach {})        ; → ("must have key a" "must have key b")
(same-map-coach {:a 1})    ; → ("must have key b")
(same-map-coach {:a true}) ; → ("value at a must be a number" "must have key b")
(same-map-coach {:b 1})    ; → ("must have key a")

;; We can also add on to a coach.
(def improved-map-coach
 (manners [my-map-coach (comp vector? :b) "value at b must be a vector"]
          ;; If the entirety of my-map-coach passes our additional check on b's
          ;; value will take place

          ;; We can also add more parallel checks
          [:c "must have key c"
           (comp string? :c) "value at c must be a string"]))

(improved-map-coach {}) ; → ("must have key a" "must have key b" "must have key c")
(improved-map-coach {:a 1}) ; → ("must have key b" "must have key c")
(improved-map-coach {:a true}) ; → ("value at a must be a number" "must have key b" "must have key c")
(improved-map-coach {:a true :b 1}) ; → ("value at a must be a number" "must have key c")
(improved-map-coach {:a 1 :b 1}) ; → ("value at b must be a vector" "must have key c")
(improved-map-coach {:a 1 :b [] :c "yo"}) ; → ()

With

To avoid having to consistently pass in etiquette as a first argument you can use the with-etiquette macro.

(use 'manners.with)
(with-etiquette [[even? "must be even"
                  #(zero? (mod % 6)) "must be divisible by 6"]
                 [#(>= % 19) "must be greater than or equal to 19"]]
  (proper? 10)      ; → false
  (invalid? 11)     ; → true
  (errors 19)       ; → ("must be even")
  (bad-manners 20)  ; → ("must be divisible by 6")
  (bad-manners 24)) ; → ()

Bellman

A town crier knows how to get a message across effectively. The manners.bellman namespace is for just that, getting the message across.

It is a set of functions for manipulating messages from coaches and creating new coaches with built in transformations. These functions are not particularly complex, any moderately experienced Clojurist could implement the same things in no time. Still, many applications of manners will find them useful so here they are, included in this library.

prefix, suffix and modify

prefix is a higher order function which may be used to add a prefix to a sequence of messages.

(require '[manners.victorian :refer [as-coach]])
(use 'manners.bellman)

(def name-coach (manner string? "must be a string"))
(def login-coach (as-coach (prefix "login ") name-coach :login))

(login-coach {:login "a string"}) ; → ()
(login-coach {:login :derp})      ; → ("login must be a string")

suffix works the same way except it appends the given string to messages instead of prepending them. modify is the more generic form of suffix and prefix. Its source is its best documentation.

at

The above usage of prefix with a map is very common. Thus, the supremely helpful at function may be used to apply a coach at some path within a map.

(def login-coach (at name-coach :login)) ; Will work out to the same as above
(def new-user-primary-login-coach (at name-coach :new-user :primary-login))

(new-user-primary-login-coach
  {:new-user {:primary-login "hmm"}}) ; → ()
(new-user-primary-login-coach
  {:new-user
    {:primary-login 'not-a-string}}) ; → ("new-user primary-login must be a string")

specifiying (including formatting and invoking)

specifiying is higher order function that creates a coach from another coach and a function to be called on messages returned by that given coach and the value the coach is called on. What?

Let's look at formatting and invoking to clarify.

;; formatting and invoking are defined simply.
(def formatting (partial specifiying format))
(def invoking (partial specifiying (fn [f v] (f v))))

Now, we can apply formatting and invoking to different coaches to see what the result is.

(def truthy-coach (formatting (manner identity "%s is not truthy")))
(truthy-coach false) ; → ("false is not truthy")
(truthy-coach nil)   ; → ("nil is not truthy")

;; Of course it is still a working coach
(truthy-coach 1) ; → ()

;; The same coach could be implemented with invoking like so:
(invoking (manner identity (fn [v] (str v " is not truthy"))))

;; Or the more generic, specifying, like so:
(specifiying (fn [m v] (str v m)) (manner identity "is not truthy"))

Really

The manners.really defines two public macros, really and verily. The minor difference is pointed out below. The purpose of these macros is to make it just a little bit easier to define coaches of a single predicate message pair.

(use 'manners.really)
((really "must be a" string?) 1) ; → ("must be a string")
((really "must be" < 10) 19)     ; → ("must be < 10")

The difference between verily and really is how trailing arguments are included in a generated message.

(def ten 10)
((really "must be" < ten) 19) ; → ("must be < 10")
((verily "must be" < ten) 19) ; → ("must be < ten")

;; Expressions work too.
(defn less-then [x] (fn [y] (< y x)))
((really "must be" (less-than ten)) 19) ; → ("must be less than ten")

Comparisons

Clojure already has quite a few good validation libraries. This library is not greatly different and has no groundbreaking features. However, it does differ a couple of key ways.

manners

  • is simple
  • feature rich
  • uses memoization for to avoid doing the same thing more than once
  • works on arbitrary values, not just maps.

The following are some descriptions of other validation libraries in the wild. They are listed alphabetically.

  • bouncer is a neat DSL for validating maps which works particularly well on nested maps. It accepts predicates as validators and even checks custom meta on those predicates for special behaviour like message formatting. A common trend in other validation libraries, it returns a custom formated data structure:

    validate takes a map and one or more validation forms and returns a vector.

    The first element in this vector contains a map of the error messages, whereas the second element contains the original map, augmented with the error messages.

  • metis is a keyword based DSL for validating maps by creating higher order functions. It returns a map reusing the validated keys with values being a sequence of errors. It advertises validator composition which works great for validating nested maps.

  • mississippi offers a lot of the same functionality by returning maps containing errors on each key. Of course that means this library is useful for validating maps only.

  • Red Tape is a form validation library. It is not meant to be general purpose at all in an effort to "reduce friction" in its problem domain.

  • sandbar is a web application library which, along with many other neat features, offers form generation with built in validation. Obviously this is also very focused on its singular use case.

  • validateur a popular library with good documentation which does much the same thing as mississippi.

  • valip is perhaps the must similar of the validation libraries to manners in that it is based on the simple application of predicates. However, its use does not differ greatly from validateur or mississippi. It also provides a helpful suite of predicates and higher-order, predicate generating functions which are compatible with manners (since they are just predicates).

  • vlad is another general purpose validation library.

    Vlad is an attempt at providing convenient and simple validations. Vlad is purely functional and makes no assumptions about your data. It can be used for validating html form data just as well as it can be used to validate your csv about cats.

    So can manners.

    I have been inspired by vald though. Its ability to compose is so valuable it seems necessary which is why I implemented something similar in manners (i.e. coaches made of coaches).

    Another interesting part of vlad is its use of protocols.

    Validations are not tied to any particular data type. They just need to implement the vlad.validation-types/Validation protocol. A default implementation has been made for clojure.lang.IFn.

    manners does not do anything like this. I am not convinced it really makes sense for manners.

Clearly validating maps is a common problem in Clojure. An example use case is a web application which needs to validate its parameters. Another is custom maps without a strongly defined (i.e. typed) schema.

Although it may be less common I find there are cases where validating arbitrary values is very useful. Many of libraries listed above do not work with non-maps nor could they be easily modified to do so because they are, by design meant for keyed data structures.

With manners there is no concept of a value having a field at all. One consequence of not requiring fields is that validating related fields is easier. Consider the following etiquette and data.

(def data {:count 3 :words ["big" "bad" "wolf"]})
(defn count-words-equal? [{cnt :count words :words}]
  (= cnt (count words)))
(def etq
  [[(comp number? :count) "count must be a number"]
   [count-words-equal? "count must equal the length of the words sequence"]])

This works fine with manners. Other libraries make validating relationships between fields much more difficult by limiting a predicate's application to the value of the field it is keyed to. The benefit of doing it that way is you can concisely define per field validations. The alternative, having to drill down to the field you mean to apply your predicate to, may seem like more work but it is still quite concise when using comp (see above example) and the bellman namespace is pretty helpful in this situation too.

Test

Vanilla and delicious clojure.test.

lein test

License

Copyright © 2014 Ryan McGowan

Distributed under the Eclipse Public License, the same as Clojure.