bouncer

0.3.0-alpha1


A validation DSL for Clojure apps

dependencies

org.clojure/clojure
1.5.1
org.clojure/algo.monads
0.1.0



(this space intentionally left almost blank)
 

The core namespace provides the two main entry point functions in bouncer:

  • validate
  • valid?

All other functions are meant for internal use only and shouldn't be relied upon.

The project README should get you started, it's pretty comprehensive.

If you'd like to know more about the motivation behind bouncer, check the announcement post.

(ns bouncer.core
  {:author "Leonardo Borges"}
  (:require [clojure.algo.monads :as m]))

Internal utility functions

(defn- build-multi-step
  ([key-or-vec fn-vec] (build-multi-step key-or-vec fn-vec []))
  ([key-or-vec [f-or-list & rest] acc]
     (if-not f-or-list
       acc
       (cond
        (sequential? f-or-list)
        (let [[f & opts] f-or-list]
          (recur key-or-vec
                 rest
                 (conj acc (concat [f key-or-vec ] opts))))
        :else (recur key-or-vec
                     rest
                     (conj acc [f-or-list key-or-vec]))))))

Takes two arguments:

parent-keyword is a :keyword - or a vector of :keywords denoting a path in a associative structure

validations-map is a map of forms following this spec:

  {:keyword [f g] :another-keyword h}

Merges :parent-keyword with every first element of validations-map, transforming it into:

  ([:parent-keyword :keyword] [f g] [:parent-keyword :another-keyword] h)
(defn- merge-path
  [parent-key validations-map]
  (let [parent-key (if (keyword? parent-key) [parent-key] parent-key)]
    (mapcat (fn [[key validations]]
              (if (vector? key)
                [(apply vector (concat parent-key key)) validations]
                [(apply vector (concat parent-key [key])) validations]))
            validations-map)))
(defn- build-steps [[head & tail :as forms]]  
  (let [forms (if (map? head)
                (vec (mapcat identity head))
                forms)]
    (reduce (fn [acc [key-or-vec sym-or-coll :as rule]]
              (cond
               (vector? sym-or-coll)
               (concat acc (build-multi-step key-or-vec sym-or-coll))
               (map? sym-or-coll)
               (concat acc (build-steps (merge-path key-or-vec
                                                    sym-or-coll)))
               :else (conj acc [sym-or-coll key-or-vec])))
            []
            (partition 2 forms))))
(defn- pre-condition-met? [pre-fn map]
  (or (nil? pre-fn) (pre-fn map)))

Wraps pred in the context of validating a single value

  • acc is the map being validated

  • pred is a validator

  • k the path to the value to be validated in the associative structure acc

  • args any extra args to pred

    It only runs pred if:

  • the validator contains a pre-condition and it is met or;

  • the validator is optional and there is a non-nil value to be validated (this information is read from pred's metadata) or;
  • there are no previous errors for the given path

    Returns acc augmented with a namespace qualified ::errors keyword

(defn- wrap
  [acc [pred k & args]]
  (let [k (if (vector? k) k [k])
        error-path (cons ::errors k)
        {:keys [default-message-format optional]
         :or {default-message-format "Custom validation failed for %s"
              optional false}} (meta pred)
        [args opts] (split-with (complement keyword?) args)
        {:keys [message pre] :or {message default-message-format}} (apply hash-map opts)
        pred-subject (get-in acc k)]
    (if (pre-condition-met? pre acc)
      (if (or (and optional (nil? pred-subject))
              (not (empty? (get-in acc error-path)))
              (apply pred pred-subject args))
        acc
        (update-in acc error-path
                   #(conj % (format message (name (peek k))))))
      acc)))

Internal Use.

Chain of responsibility.

Takes a collection of validators fs and returns a state monad-compatible function.

This function will run all validators against old-state and eventually return a vector with the result - the errors map - and the new state - the original map augmented with the errors map.

See also wrap

(defn- wrap-chain
  [& fs]
  (fn [old-state]
    (let [new-state (reduce wrap
                            old-state
                            fs)]
      [(::errors new-state) new-state])))

Internal use.

Validates the map m using the validation functions fs.

Returns a vector where the first element is the map of validation errors if any and the second is the original map (possibly)augmented with the errors map.

(defn- validate*
  [m fs]
  (letfn [(m-fn [fs]
            (let [m-bind (:m-bind m/state-m)
                  m-result (:m-result m/state-m)]
              (cond
               (> (count fs) 1) (m-bind (bouncer.core/wrap-chain (first fs))
                                        (fn [_]
                                          (m-fn (rest fs))))
               :else (m-bind (bouncer.core/wrap-chain (first fs))
                             (fn [result]
                               (m-result result))))))]
    ((m-fn fs) m)))

Public API

Validates the map m using the validations specified by forms.

forms can be a single validator set or a sequence of key/value pairs where:

key ==> :keyword or [:a :path]

value ==> validation-function or validator-set or [[validation-function args+opts]] or [validation-function another-validation-function] or [validation-function [another-validation-function args+opts]]

e.g.:

  (core/validate a-map
           :name core/required
           :age  [core/required
                 [core/number :message "age must be a number"]]
           [:passport :number] core/positive)

Returns a vector where the first element is the map of validation errors if any and the second is the original map (possibly)augmented with the errors map.

See also defvalidator

(defn validate
  [m & forms]
  (validate* m (build-steps forms)))

Takes a map and one or more validation functions with semantics provided by "validate". Returns true if the map passes all validations. False otherwise.

(defn valid?
  [& args]
  (empty? (first (apply validate args))))
 

This namespace contains all built-in validators as well as macros for defining new validators and validator sets

(ns bouncer.validators
  {:author "Leonardo Borges"}
  (:require [clojure.walk :as w]))

Customization support

The following functions and macros support creating custom validators

Defines a new validating function using args & body semantics as provided by "defn". docstring and opts-map are optional

opts-map is a map of key-value pairs and may be one of:

:default-message-format used when the client of this validator doesn't provide a message

:optional whether the validation should be run only if the given key has a non-nil value in the map. Defaults to false.

The function will be called with the value being validated as its first argument.

Any extra arguments will be passed along to the function in the order they were used in the "validate" call.

e.g.:

(defvalidator member
  [value coll]
  (some #{value} coll))

(validate {:age 10}
  :age [[member (range 5)]])

This means the validator member will be called with the arguments 10 and (0 1 2 3 4), in that order.

(defmacro defvalidator
  {:arglists '([name docstring? opts-map? [args*] & body])}
  [name & options]
  (let [docstring (if (string? (first options))
                    (first options)
                    nil)
        options (if (string? (first options))
                  (next options)
                  options)
        {
         :keys [default-message-format optional]
         :or {
              default-message-format "Custom validation failed for %s"
              optional false}
         } (if (map? (first options))
             (first options)
             {})
        options (if (map? (first options))
                  (next options)
                  options)
        [args & body] options
        fn-meta {:default-message-format default-message-format
                 :optional optional}]
    (let [arglists ''([name])]
      `(do (def ~name
             (with-meta (fn ~name 
                          ([~@args]
                             ~@body))
               (merge ~fn-meta)))
           (alter-meta! (var ~name) assoc
                        :doc ~docstring
                        :arglists '([~@args]))))))

Built-in validators

Validates value is present.

If the value is a string, it makes sure it's not empty, otherwise it checks for nils.

For use with validation functions such as validate or valid?

(defvalidator required
  {:default-message-format "%s must be present"}
  [value]
  (if (string? value)
    (not (empty? value))
    (not (nil? value))))

Validates maybe-a-number is a valid number.

For use with validation functions such as validate or valid?

(defvalidator number
  {:default-message-format "%s must be a number" :optional true}
  [maybe-a-number]
  (number? maybe-a-number))

Validates number is a number and is greater than zero.

For use with validation functions such as validate or valid?

(defvalidator positive
  {:default-message-format "%s must be a positive number" :optional true}
  [number]
  (> number 0))

Validates value is a member of coll.

For use with validation functions such as validate or valid?

(defvalidator member
  {:default-message-format "%s must be one of the values in the list"}
  [value coll]
  (some #{value} coll))

Validates pred is true for the given value.

For use with validation functions such as validate or valid?

(defvalidator custom
  [value pred]
  (println "Warning: bouncer.validators/custom is deprecated and will be removed. Use plain functions instead.")
  (pred value))

Validates pred is true for every item in coll.

For use with validation functions such as validate or valid?

(defvalidator every
  {:default-message-format "All items in %s must satisfy the predicate"}
  [coll pred]
  (every? pred coll))

Validates value satisfies the given regex pattern.

For use with validation functions such as validate or valid?

(defvalidator matches
  {:default-message-format "%s must satisfy the given pattern" :optional true}
  [value re]
  ((complement empty?) (re-seq re value)))