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.
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:
(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:
Identifier | Coercion | Description |
---|---|---|
|
no |
Validator for clojure’s keyword |
|
no |
Validator for UUID’s |
|
yes |
Validator for uuid strings with coercion to UUID |
|
no |
Validator for email string. |
|
no |
Marks field as required. |
|
no |
Validator for Number. |
|
yes |
Validator for number string. |
|
no |
Validator for integer. |
|
yes |
Validator for integer string. |
|
no |
Validator for boolean. |
|
yes |
Validator for boolean string. |
|
no |
Validator for string. |
|
yes |
Validator for string like. |
|
no |
Validator for a number range. |
|
no |
Validator for check if a value is member of coll. |
|
no |
Validator for positive number. |
|
no |
Validator for negative number. |
|
no |
Validator for IFn interface. |
|
no |
Validator for clojure vector. |
|
no |
Validator for clojure map. |
|
no |
Validator for clojure set. |
|
no |
Validator for clojure coll. |
|
no |
Validator to check if pred match for every item in coll. |
|
no |
Validator to check that value is identical to other field. |
|
no |
Validator to check that value is has at least a minimum number of characters. |
|
no |
Validator to check that value is not larger than a maximum number of characters. |
Additional notes:
-
number-str
coerces tojava.lang.Double
orfloat
(cljs) -
boolean-str
coerces totrue
("t"
,"true"
,"1"
) orfalse
("f"
,"false"
,"0"
). -
string-str
coerces anything to string usingstr
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/>