RyanMcG.manners

https://github.com/RyanMcG/manners.git

git clone 'https://github.com/RyanMcG/manners.git'

(ql:quickload :RyanMcG.manners)
21

manners Build Status

A validation library built on using predicates properly.

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

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.

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: clojure (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

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

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.