Introduction

A structural validation library for Clojure and ClojureScript.

Highlights:

  • No macros: validators are defined using plain data.

  • Dependent validators: the ability to access to already validated data.

  • Coercion: the ability to coerce incoming values to other types.

  • No exceptions: no exceptions used in the validation process.

Based on similar ideas of bouncer.

Project Maturity

Since struct is a young project there may be some API breakage.

Install

Just include that in your dependency vector on project.clj:

[funcool/struct "1.3.0"]

User guide

Quick Start

Let’s require the main struct namespace:

(require '[struct.core :as st])

Define a small schema for the example purpose:

(def +scheme+
  {:name [st/required st/string]
   :year [st/required st/number]})

You can observe that it consists in a simple map when you declare keys and corresponding validators for that key. A vector as value allows us to put more than one validator for the same key. If you have only one validator for the key, you can omit the vector and put it as single value.

The same schema can be defined using vectors, if the order of validation matters:

(def +scheme+
  [[:name st/required st/string]
   [:year st/required st/number]])

By default, all validators are optional so if the value is missing, no error will reported. If you want make the value mandatory, you should use a specific required validator.

And finally, start validating your data:

(-> {:name "Blood of Elves" :year 1994}
    (st/validate +scheme+))
;; => [nil {:name "Blood of Elves" :year 1994}]

(-> {:name "Blood of Elves" :year "1994"}
    (st/validate +scheme+))
;; => [{:year "must be a number"} {:name "Blood of Elves", :year "1994"}]

(-> {:year "1994"}
    (st/validate +scheme+))
;; => [{:name "this field is mandatory", :year "must be a number"} {}]

If only want to know if some data is valid or not, you can use the valid? predicate for that purpose:

(st/valid? {:year "1994"} +scheme+)
;; => false

The additional entries in the map are not stripped by default, but this behavior can be changed passing an additional flag as the third argument:

(-> {:name "Blood of Elves" :year 1994 :foo "bar"}
    (st/validate +scheme+))
;; => [nil {:name "Blood of Elves" :year 1994 :foo "bar"}]

(-> {:name "Blood of Elves" :year 1994 :foo "bar"}
    (st/validate +scheme+ {:strip true}))
;; => [nil {:name "Blood of Elves" :year 1994}]

With similar syntax you can validate neested data structures, specifying in the key part the proper path to the neested data structure:

(def +scheme+
  {[:a :b] st/integer
   [:c :d] st/string})

(-> {:a {:b "foo"} {:c {:d "bar"}}}
    (st/validate +scheme+))
;; => [{:a {:b "must be a number"}} {:c {:d "bar"}}]

Parametrized validators

In addition to simple validators, one may use additional contraints (e.g. in-range). This is how they can be passed to the validator:

(def schema {:num [[st/in-range 10 20]]})

(st/validate {:num 21} schema)
;; => [{:num "not in range"} {}]

(st/validate {:num 19} schema)
;; => [nil {:num 19}]

Note the double vector; the outer denotes a list of validatiors and the inner denotes a validator with patameters.

Custom messages

The builtin validators comes with default messages in human readable format, but sometimes you may want to change them (e.g. for i18n purposes). This is how you can do it:

(def schema
  {:num [[st/in-range 10 20 :message "errors.not-in-range"]]})

(st/validate {:num 21} schema)
;; => [{:num "errors.not-in-range"} {}]

A message can contains format wildcards %s, these wildcards will be replaced by args of validator, e.g.:

(def schema
  {:age [[st/in-range 18 26 :message "The age must be between %s and %s"]]})

(st/validate {:age 30} schema)
;; => [{:age "The age must be between 18 and 26"} {}]

Data coercions

In addition to simple validations, this library includes the ability to coerce values, and a collection of validators that matches over strings. Let’s see some code:

Example attaching custom coercions
(def schema
  {:year [[st/integer :coerce str]]})

(st/validate {:year 1994} schema))
;; => [nil {:year "1994"}]

Looking at the data returned from the validation process, one can see that the value is properly coerced with the specified coercion function.

This library comes with a collection of validators that already have attached coercion functions. These serve to validate parameters that arrive as strings but need to be converted to the appropriate type:

(def schema {:year [st/required st/integer-str]
             :id [st/required st/uuid-str]})

(st/validate {:year "1994"
              :id "543e7472-6624-4cb5-b65e-f3c341843d0f"}
             schema)
;; => [nil {:year 1994, :id #uuid "543e7472-6624-4cb5-b65e-f3c341843d0f"}]

To facilitate this operation, the validate! function receives the data and schema, then returns the resulting data. If data not matches the schema an exception will be raised using ex-info clojure facility:

(st/validate! {:year "1994" :id "543e7472-6624-4cb5-b65e-f3c341843d0f"} schema)
;; => {:year 1994, :id #uuid "543e7472-6624-4cb5-b65e-f3c341843d0f"}

Builtin Validators

This is the table with available builtin validators:

Table 1. Builtin Validators
Identifier Coercion Description

struct.core/keyword

no

Validator for clojure’s keyword

struct.core/uuid

no

Validator for UUID’s

struct.core/uuid-str

yes

Validator for uuid strings with coercion to UUID

struct.core/email

no

Validator for email string.

struct.core/required

no

Marks field as required.

struct.core/number

no

Validator for Number.

struct.core/number-str

yes

Validator for number string.

struct.core/integer

no

Validator for integer.

struct.core/integer-str

yes

Validator for integer string.

struct.core/boolean

no

Validator for boolean.

struct.core/boolean-str

yes

Validator for boolean string.

struct.core/string

no

Validator for string.

struct.core/string-str

yes

Validator for string like.

struct.core/in-range

no

Validator for a number range.

struct.core/member

no

Validator for check if a value is member of coll.

struct.core/positive

no

Validator for positive number.

struct.core/negative

no

Validator for negative number.

struct.core/function

no

Validator for IFn interface.

struct.core/vector

no

Validator for clojure vector.

struct.core/map

no

Validator for clojure map.

struct.core/set

no

Validator for clojure set.

struct.core/coll

no

Validator for clojure coll.

struct.core/every

no

Validator to check if pred match for every item in coll.

struct.core/identical-to

no

Validator to check that value is identical to other field.

struct.core/min-count

no

Validator to check that value is has at least a minimum number of characters.

struct.core/max-count

no

Validator to check that value is not larger than a maximum number of characters.

Additional notes:

  • number-str coerces to java.lang.Double or float (cljs)

  • boolean-str coerces to true ("t", "true", "1") or false ("f", "false", "0").

  • string-str coerces anything to string using str function.

Define your own validator

As mentioned previously, the validators in struct library are defined using plain hash-maps. For example, this is how the builtin integer validator is defined:

(def integer
  {:message "must be a integer"
   :optional true
   :validate integer?}))

If the validator needs access to previously validated data, the :state key should be present with the value true. Let see the identical-to validator as example:

(def identical-to
  {:message "does not match"
   :optional true
   :state true
   :validate (fn [state v ref]
               (let [prev (get state ref)]
                 (= prev v)))})

Validators that access the state receive an additional argument with the state for validator function.

Developers Guide

Contributing

Unlike Clojure and other Clojure contrib libs, there aren’t many restrictions for contributions. Just open an issue or pull request.

Get the Code

struct is open source and can be found on github.

You can clone the public repository with this command:

git clone https://github.com/funcool/struct

Run tests

To run the tests execute the following:

For the JVM platform:

lein test

And for JS platform:

./scripts/build
node out/tests.js

You will need to have nodejs installed on your system.

License

struct is under public domain:

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>