bouncer

0.3.2


A validation DSL for Clojure apps

dependencies

org.clojure/clojure
1.6.0
clj-time
0.9.0
com.andrewmcveigh/cljs-time
0.3.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"})

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
  [message-fn acc [pred k & args]]
  (let [k (if (vector? k) k [k])
        error-path (cons ::errors k)
        {:keys [optional default-message-format]
         :or {optional false
              default-message-format "Custom validation failed for %s"}
         :as metadata} (meta pred)
        meta-with-defaults
        (merge metadata {:default-message-format default-message-format
                         :optional optional})
        [args opts] (split-with (complement keyword?) args)
        {:keys [message pre]} (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 % (message-fn {:path k, :value pred-subject
                                         :args (seq args)
                                         :metadata meta-with-defaults
                                         :message message}))))
      acc)))

Internal Use.

Chain of responsibility.

Takes the current state and a collection of validators fs

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
  [old-state message-fn & fs]
  (let [new-state (reduce (partial wrap message-fn)
                          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*
  [message-fn m fs]
  (loop [[errors state :as ret] [nil m]
         fs fs]
    (if (seq fs)
      (recur (wrap-chain state message-fn (first fs)) (rest fs))
      ret)))

Public API

Use together with validate, e.g.:

  (core/validate core/with-default-messages {}
                 :name v/required)
(defn with-default-messages
  [error]
  (let [{:keys [message path metadata]} error]
          (format (or message (:default-message-format metadata))
                  (name (peek path)))))

Takes a

  • message-fn (optional) responsible for transforming error metadata into the validation result (defaults to with-default-messages)

  • m map to be validated

  • forms validations to be performed on the map

    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 v/required :age [v/required [v/number :message "age must be a number"]] [:passport :number] v/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
  [& args]
  (let [[message-fn args] (if (fn? (first args))
                                   [(first args) (next args)]
                                   [with-default-messages args])
        [m forms] [(first args) (next args)]]
    (validate* message-fn 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 file autogenerated from src/bouncer/core.cljx

 

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 [clj-time.format :as f]))

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 (consider using custom message functions)

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

    or any other key-value pair which will be available in the validation result under the :metadata key.

    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 options] (if (string? (first options))
                                 [(first options) (next options)]
                                 [nil options])
        [fn-meta [args & body]] (if (map? (first options))
                                  [(first options) (next options)]
                                  [nil options])
        fn-meta (assoc fn-meta
                       :validator (keyword (str *ns*) (str name)))]
    (let [arglists ''([name])]
      `(do (def ~name (with-meta (fn ~name
                                   ([~@args]
                                    ~@body)) ~fn-meta))))))

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)))

Validates value is an email address.

It implements a simple check to verify there's only a '@' and at least one point after the '@'

For use with validation functions such as validate or valid?

(defvalidator email
  {:default-message-format "%s must be a valid email address"}
  [value]
  (and (required value) (matches value #"^[^@]+@[^@\\.]+[\\.].+")))

Validates value is a date(time).

Optionally, takes a formatter argument which may be either an existing clj-time formatter, or a string representing a custom datetime formatter.

For use with validation functions such as validate or valid?

(defvalidator datetime
  {:default-message-format "%s must be a valid date"}
  [value & [opt & _]]
  (let [formatter (if (string? opt) (f/formatter opt) opt)]
    (try
      (if formatter (f/parse formatter value) (f/parse value))
            (catch IllegalArgumentException e false))))

Validates value is not greater than a max count

For use with validation functions such as validate or valid?

(defvalidator max-count
  {:default-message-format "%s is longer than the maximum"}
  [value maximum]
  (<= (count (seq value)) maximum))

Validates value at least meets the minimum count

For use with validation functions such as validate or valid?

(defvalidator min-count
  {:default-message-format "%s is less than the minimum"}
  [value minimum]
  (>= (count (seq value)) minimum))

This file autogenerated from src/bouncer/validators.cljx