├── .gitignore
├── README.md
├── doc
├── content-docinfo.html
├── Makefile
└── content.adoc
├── CONTRIBUTING.md
├── deps.edn
├── CHANGELOG.md
├── UNLICENSE
├── tools.clj
├── test
└── struct
│ └── tests.cljc
└── src
└── struct
└── core.cljc
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | /*-init.clj
11 | /doc/dist
12 | /out
13 | /repl
14 | /node_modules
15 | /settings.xml
16 | /.cpcache
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # struct #
2 |
3 | A structural validation library for Clojure(Script).
4 |
5 | [](http://clojars.org/funcool/struct)
6 |
7 | Documentation: http://funcool.github.io/struct/latest/
8 |
--------------------------------------------------------------------------------
/doc/content-docinfo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | all: doc
2 |
3 | api:
4 | cd ..
5 | lein doc
6 |
7 | doc:
8 | mkdir -p dist/latest/
9 | asciidoctor -a docinfo -a stylesheet! -o dist/latest/index.html content.adoc
10 |
11 | github: doc api
12 | ghp-import -m "Generate documentation" -b gh-pages dist/
13 | git push origin gh-pages
14 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributed Code #
2 |
3 | In order to keep *struct* completely free and unencumbered by copyright, all new
4 | contributors to the *struct* code base are asked to dedicate their contributions to
5 | the public domain. If you want to send a patch or enhancement for possible inclusion
6 | in the *struct* source tree, please accompany the patch with the following
7 | statement:
8 |
9 | The author or authors of this code dedicate any and all copyright interest
10 | in this code to the public domain. We make this dedication for the benefit of
11 | the public at large and to the detriment of our heirs and successors. We
12 | intend this dedication to be an overt act of relinquishment in perpetuity of
13 | all present and future rights to this code under copyright law.
14 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:deps {funcool/cuerdas {:mvn/version "2.2.0"}}
2 | :paths ["src"]
3 | :aliases
4 | {:dev
5 | {:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.516"}
6 | ;; org.clojure/clojure {:mvn/version "1.10.0"}
7 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}
8 | com.bhauman/rebel-readline {:mvn/version "0.1.4"}
9 | com.bhauman/figwheel-main {:mvn/version "0.2.0"}
10 | eftest/eftest {:mvn/version "0.5.7"}}
11 | :extra-paths ["test"]}
12 |
13 | :ancient {:main-opts ["-m" "deps-ancient.deps-ancient"]
14 | :extra-deps {deps-ancient {:mvn/version "RELEASE"}}}
15 |
16 | :jar {:extra-deps {seancorfield/depstar {:mvn/version "RELEASE"}}
17 | :main-opts ["-m" "hf.depstar.jar"]}
18 |
19 | :repl {:main-opts ["-m" "rebel-readline.main"]}
20 | }}
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog #
2 |
3 | ## Version 1.3.0 ##
4 |
5 | Date: 2018-06-02
6 |
7 | - Fix message formatting.
8 |
9 |
10 | ## Version 1.2.0 ##
11 |
12 | Date: 2017-01-11
13 |
14 | - Allow `number-str` and `integer-str` receive already coerced values.
15 | - Minor code cleaning.
16 | - Update dependencies.
17 |
18 | ## Version 1.1.0 ##
19 |
20 | Date: 2017-08-16
21 |
22 | - Add count validators.
23 | - Update cuerdas to 2.0.3
24 |
25 |
26 | ## Version 1.0.0 ##
27 |
28 | Date: 2016-06-24
29 |
30 | - Add support for neested data structures.
31 | - Add fast skip already validated and failed paths (performance improvement).
32 | - BREAKING CHANGE: the errors are now simple strings. No additional list
33 | wrapping is done anymore. Because the design of the library is just fail
34 | fast and only one error is allowed.
35 |
36 |
37 | ## Version 0.1.0 ##
38 |
39 | Date: 2016-04-19
40 |
41 | Initial version.
42 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
--------------------------------------------------------------------------------
/tools.clj:
--------------------------------------------------------------------------------
1 | (require '[clojure.java.shell :as shell]
2 | '[clojure.main])
3 | (require '[rebel-readline.core]
4 | '[rebel-readline.clojure.main]
5 | '[rebel-readline.clojure.line-reader]
6 | '[rebel-readline.clojure.service.local]
7 | '[rebel-readline.cljs.service.local]
8 | '[rebel-readline.cljs.repl]
9 | '[eftest.runner :as ef])
10 | (require '[cljs.build.api :as api]
11 | '[cljs.repl :as repl]
12 | '[cljs.repl.node :as node])
13 |
14 | (defmulti task first)
15 |
16 | (defmethod task :default
17 | [args]
18 | (let [all-tasks (-> task methods (dissoc :default) keys sort)
19 | interposed (->> all-tasks (interpose ", ") (apply str))]
20 | (println "Unknown or missing task. Choose one of:" interposed)
21 | (System/exit 1)))
22 |
23 | (defmethod task "repl"
24 | [[_ type]]
25 | (case type
26 | (nil "clj")
27 | (rebel-readline.core/with-line-reader
28 | (rebel-readline.clojure.line-reader/create
29 | (rebel-readline.clojure.service.local/create))
30 | (clojure.main/repl
31 | :prompt (fn []) ;; prompt is handled by line-reader
32 | :read (rebel-readline.clojure.main/create-repl-read)))
33 |
34 | "node"
35 | (rebel-readline.core/with-line-reader
36 | (rebel-readline.clojure.line-reader/create (rebel-readline.cljs.service.local/create))
37 | (cljs.repl/repl
38 | (node/repl-env)
39 | :prompt (fn []) ;; prompt is handled by line-reader
40 | :read (rebel-readline.cljs.repl/create-repl-read)
41 | :output-dir "out"
42 | :cache-analysis false))
43 | (println "Unknown repl: " type)
44 | (System/exit 1)))
45 |
46 | (def options
47 | {:main 'struct.tests
48 | :output-to "out/tests.js"
49 | :output-dir "out/tests"
50 | :target :nodejs
51 | :pretty-print true
52 | :optimizations :advanced
53 | :language-in :ecmascript5
54 | :language-out :ecmascript5
55 | :verbose true})
56 |
57 | (defmethod task "test"
58 | [[_ exclude]]
59 | (let [tests (ef/find-tests "test")
60 | tests (if (string? exclude)
61 | (ef/find-tests (symbol exclude))
62 | tests)]
63 | (ef/run-tests tests
64 | {:fail-fast? true
65 | :capture-output? false
66 | :multithread? false})
67 | (System/exit 1)))
68 |
69 |
70 | (defmethod task "test-cljs"
71 | [[_ type]]
72 | (letfn [(build [optimizations]
73 | (api/build (api/inputs "src" "test")
74 | (cond-> (assoc options :optimizations optimizations)
75 | (= optimizations :none) (assoc :source-map true))))
76 |
77 | (run-tests []
78 | (let [{:keys [out err]} (shell/sh "node" "out/tests.js")]
79 | (println out err)))
80 |
81 | (test-once []
82 | (build :none)
83 | (run-tests)
84 | (shutdown-agents))
85 |
86 | (test-watch []
87 | (println "Start watch loop...")
88 | (try
89 | (api/watch (api/inputs "src", "test")
90 | (assoc options
91 | :parallel-build false
92 | :watch-fn run-tests
93 | :cache-analysis false
94 | :optimizations :none
95 | :source-map true))
96 | (catch Exception e
97 | (println "ERROR:" e)
98 | (Thread/sleep 2000)
99 | (test-watch))))]
100 |
101 | (case type
102 | (nil "once") (test-once)
103 | "watch" (test-watch)
104 | "build-none" (build :none)
105 | "build-simple" (build :simple)
106 | "build-advanced" (build :advanced)
107 | (do (println "Unknown argument to test task:" type)
108 | (System/exit 1)))))
109 |
110 | ;;; Build script entrypoint. This should be the last expression.
111 |
112 | (task *command-line-args*)
113 |
--------------------------------------------------------------------------------
/test/struct/tests.cljc:
--------------------------------------------------------------------------------
1 | (ns struct.tests
2 | (:require #?(:cljs [cljs.test :as t]
3 | :clj [clojure.test :as t])
4 | [struct.core :as st]))
5 |
6 | ;; --- Tests
7 |
8 | (t/deftest test-optional-validators
9 | (let [scheme {:max st/number
10 | :scope st/string}
11 | input {:scope "foobar"}
12 | result (st/validate input scheme)]
13 | (t/is (= nil (first result)))
14 | (t/is (= input (second result)))))
15 |
16 | (t/deftest test-simple-validators
17 | (let [scheme {:max st/number
18 | :scope st/string}
19 | input {:scope "foobar" :max "d"}
20 | errors {:max "must be a number"}
21 | result (st/validate input scheme)]
22 | (t/is (= errors (first result)))
23 | (t/is (= {:scope "foobar"} (second result)))))
24 |
25 | (t/deftest test-neested-validators
26 | (let [scheme {[:a :b] st/number
27 | [:c :d :e] st/string}
28 | input {:a {:b "foo"} :c {:d {:e "bar"}}}
29 | errors {:a {:b "must be a number"}}
30 | result (st/validate input scheme)]
31 | (t/is (= errors (first result)))
32 | (t/is (= {:c {:d {:e "bar"}}} (second result)))))
33 |
34 | (t/deftest test-single-validators
35 | (let [result1 (st/validate-single 2 st/number)
36 | result2 (st/validate-single nil st/number)
37 | result3 (st/validate-single nil [st/required st/number])]
38 | (t/is (= [nil 2] result1))
39 | (t/is (= [nil nil] result2))
40 | (t/is (= ["this field is mandatory" nil] result3))))
41 |
42 | (t/deftest test-parametric-validators
43 | (let [result1 (st/validate
44 | {:name "foo"}
45 | {:name [[st/min-count 4]]})
46 | result2 (st/validate
47 | {:name "bar"}
48 | {:name [[st/max-count 2]]})]
49 | (t/is (= {:name "less than the minimum 4"} (first result1)))
50 | (t/is (= {:name "longer than the maximum 2"} (first result2)))))
51 |
52 | (t/deftest test-simple-validators-with-vector-schema
53 | (let [scheme [[:max st/number]
54 | [:scope st/string]]
55 | input {:scope "foobar" :max "d"}
56 | errors {:max "must be a number"}
57 | result (st/validate input scheme)]
58 | (t/is (= errors (first result)))
59 | (t/is (= {:scope "foobar"} (second result)))))
60 |
61 | (t/deftest test-simple-validators-with-translate
62 | (let [scheme [[:max st/number]
63 | [:scope st/string]]
64 | input {:scope "foobar" :max "d"}
65 | errors {:max "a"}
66 | result (st/validate input scheme {:translate (constantly "a")})]
67 | (t/is (= errors (first result)))
68 | (t/is (= {:scope "foobar"} (second result)))))
69 |
70 | (t/deftest test-dependent-validators-1
71 | (let [scheme [[:password1 st/string]
72 | [:password2 [st/identical-to :password1]]]
73 | input {:password1 "foobar"
74 | :password2 "foobar."}
75 | errors {:password2 "does not match"}
76 | result (st/validate input scheme)]
77 | (t/is (= errors (first result)))
78 | (t/is (= {:password1 "foobar"} (second result)))))
79 |
80 | (t/deftest test-dependent-validators-2
81 | (let [scheme [[:password1 st/string]
82 | [:password2 [st/identical-to :password1]]]
83 | input {:password1 "foobar"
84 | :password2 "foobar"}
85 | result (st/validate input scheme)]
86 | (t/is (= nil (first result)))
87 | (t/is (= {:password1 "foobar"
88 | :password2 "foobar"} (second result)))))
89 |
90 | (t/deftest test-multiple-validators
91 | (let [scheme {:max [st/required st/number]
92 | :scope st/string}
93 | input {:scope "foobar"}
94 | errors {:max "this field is mandatory"}
95 | result (st/validate input scheme)]
96 | (t/is (= errors (first result)))
97 | (t/is (= {:scope "foobar"} (second result)))))
98 |
99 | (t/deftest test-validation-with-coersion
100 | (let [scheme {:max st/integer-str
101 | :scope st/string}
102 | input {:max "2" :scope "foobar"}
103 | result (st/validate input scheme)]
104 | (t/is (= nil (first result)))
105 | (t/is (= {:max 2 :scope "foobar"} (second result)))))
106 |
107 | (t/deftest test-validation-with-custom-coersion
108 | (let [scheme {:max [[st/number-str :coerce (constantly :foo)]]
109 | :scope st/string}
110 | input {:max "2" :scope "foobar"}
111 | result (st/validate input scheme)]
112 | (t/is (= nil (first result)))
113 | (t/is (= {:max :foo :scope "foobar"} (second result)))))
114 |
115 | (t/deftest test-validation-with-custom-message
116 | (let [scheme {:max [[st/number-str :message "custom msg"]]
117 | :scope st/string}
118 | input {:max "g" :scope "foobar"}
119 | errors {:max "custom msg"}
120 | result (st/validate input scheme)]
121 | (t/is (= errors (first result)))
122 | (t/is (= {:scope "foobar"} (second result)))))
123 |
124 | (t/deftest test-coersion-with-valid-values
125 | (let [scheme {:a st/number-str
126 | :b st/integer-str}
127 | input {:a 2.3 :b 3.3}
128 | [errors data] (st/validate input scheme)]
129 | (t/is (= {:a 2.3 :b 3} data))))
130 |
131 | (t/deftest test-validation-nested-data-in-a-vector
132 | (let [scheme {:a [st/vector [st/every number?]]}
133 | input1 {:a [1 2 3 4]}
134 | input2 {:a [1 2 3 4 "a"]}
135 | [errors1 data1] (st/validate input1 scheme)
136 | [errors2 data2] (st/validate input2 scheme)]
137 | (t/is (= data1 input1))
138 | (t/is (= errors1 nil))
139 | (t/is (= data2 {}))
140 | (t/is (= errors2 {:a "must match the predicate"}))))
141 |
142 | (t/deftest test-in-range-validator
143 | (t/is (= {:age "not in range 18 and 26"}
144 | (-> {:age 17}
145 | (st/validate {:age [[st/in-range 18 26]]})
146 | first))))
147 |
148 | (t/deftest test-honor-nested-data
149 | (let [scheme {[:a :b] [st/required
150 | st/string
151 | [st/min-count 2 :message "foobar"]
152 | [st/max-count 5]]}
153 | input1 {:a {:b "abcd"}}
154 | input2 {:a {:b "abcdefgh"}}
155 | input3 {:a {:b "a"}}
156 | [errors1 data1] (st/validate input1 scheme)
157 | [errors2 data2] (st/validate input2 scheme)
158 | [errors3 data3] (st/validate input3 scheme)]
159 | (t/is (= data1 input1))
160 | (t/is (= errors1 nil))
161 | (t/is (= data2 {}))
162 | (t/is (= errors2 {:a {:b "longer than the maximum 5"}}))
163 | (t/is (= data3 {}))
164 | (t/is (= errors3 {:a {:b "foobar"}}))))
165 |
166 | ;; --- Entry point
167 |
168 | #?(:cljs
169 | (do
170 | (enable-console-print!)
171 | (set! *main-cli-fn* #(t/run-tests))))
172 |
173 | #?(:cljs
174 | (defmethod t/report [:cljs.test/default :end-run-tests]
175 | [m]
176 | (if (t/successful? m)
177 | (set! (.-exitCode js/process) 0)
178 | (set! (.-exitCode js/process) 1))))
179 |
--------------------------------------------------------------------------------
/src/struct/core.cljc:
--------------------------------------------------------------------------------
1 | (ns struct.core
2 | (:refer-clojure :exclude [keyword uuid vector boolean long map set])
3 | (:require [cuerdas.core :as str]))
4 |
5 | ;; --- Impl details
6 |
7 | (def ^:private map' #?(:cljs cljs.core/map
8 | :clj clojure.core/map))
9 |
10 | (defn- apply-validation
11 | [step data value]
12 | (if-let [validate (:validate step nil)]
13 | (let [args (:args step [])]
14 | (if (:state step)
15 | (apply validate data value args)
16 | (apply validate value args)))
17 | true))
18 |
19 | (defn- dissoc-in
20 | [m [k & ks :as keys]]
21 | (if ks
22 | (if-let [nextmap (get m k)]
23 | (let [newmap (dissoc-in nextmap ks)]
24 | (if (seq newmap)
25 | (assoc m k newmap)
26 | (dissoc m k)))
27 | m)
28 | (dissoc m k)))
29 |
30 | (defn- prepare-message
31 | [opts step]
32 | (if (::nomsg opts)
33 | ::nomsg
34 | (let [msg (:message step "errors.invalid")
35 | tr (:translate opts identity)]
36 | (apply str/format (tr msg) (vec (:args step))))))
37 |
38 | (def ^:const ^:private opts-params
39 | #{:coerce :message :optional})
40 |
41 | (def ^:private notopts?
42 | (complement opts-params))
43 |
44 | (defn- build-step
45 | [key item]
46 | (letfn [(coerce-key [key] (if (vector? key) key [key]))]
47 | (if (vector? item)
48 | (let [validator (first item)
49 | result (split-with notopts? (rest item))
50 | args (first result)
51 | opts (apply hash-map (second result))]
52 | (merge (assoc validator :args args :path (coerce-key key))
53 | (select-keys opts [:coerce :message :optional])))
54 | (assoc item :args [] :path (coerce-key key)))))
55 |
56 | (defn- normalize-step-map-entry
57 | [acc key value]
58 | (if (vector? value)
59 | (reduce #(conj! %1 (build-step key %2)) acc value)
60 | (conj! acc (build-step key value))))
61 |
62 | (defn- normalize-step-entry
63 | [acc [key & values]]
64 | (reduce #(conj! %1 (build-step key %2)) acc values))
65 |
66 | (defn- build-steps
67 | [schema]
68 | (cond
69 | (vector? schema)
70 | (persistent!
71 | (reduce normalize-step-entry (transient []) schema))
72 |
73 | (map? schema)
74 | (persistent!
75 | (reduce-kv normalize-step-map-entry (transient []) schema))
76 |
77 | :else
78 | (throw (ex-info "Invalid schema." {}))))
79 |
80 | (defn- strip-values
81 | [data steps]
82 | (reduce (fn [acc path]
83 | (let [value (get-in data path ::notexists)]
84 | (if (not= value ::notexists)
85 | (assoc-in acc path value)
86 | acc)))
87 | {}
88 | (into #{} (map' :path steps))))
89 |
90 | (defn- validate-internal
91 | [data steps opts]
92 | (loop [skip #{}
93 | errors nil
94 | data data
95 | steps steps]
96 | (if-let [step (first steps)]
97 | (let [path (:path step)
98 | value (get-in data path)]
99 | (cond
100 | (contains? skip path)
101 | (recur skip errors data (rest steps))
102 |
103 | (and (nil? value) (:optional step))
104 | (recur skip errors data (rest steps))
105 |
106 | (apply-validation step data value)
107 | (let [value ((:coerce step identity) value)]
108 | (recur skip errors (assoc-in data path value) (rest steps)))
109 |
110 | :else
111 | (let [message (prepare-message opts step)]
112 | (recur (conj skip path)
113 | (assoc-in errors path message)
114 | (dissoc-in data path)
115 | (rest steps)))))
116 | [errors data])))
117 |
118 | ;; --- Public Api
119 |
120 | (defn validate
121 | "Validate data with specified schema.
122 |
123 | This function by default strips all data that are not defined in
124 | schema, but this behavior can be changed by passing `{:strip false}`
125 | as third argument."
126 | ([data schema]
127 | (validate data schema nil))
128 | ([data schema {:keys [strip]
129 | :or {strip false}
130 | :as opts}]
131 | (let [steps (build-steps schema)
132 | data (if strip (strip-values data steps) data)]
133 | (validate-internal data steps opts))))
134 |
135 | (defn validate-single
136 | "A helper that used just for validate one value."
137 | ([data schema] (validate-single data schema nil))
138 | ([data schema opts]
139 | (let [data {:field data}
140 | steps (build-steps {:field schema})]
141 | (mapv :field (validate-internal data steps opts)))))
142 |
143 | (defn validate!
144 | "Analogous function to the `validate` that instead of return
145 | the errors, just raise a ex-info exception with errors in case
146 | them are or just return the validated data.
147 |
148 | This function accepts the same parameters as `validate` with
149 | an additional `:message` that serves for customize the exception
150 | message."
151 | ([data schema]
152 | (validate! data schema nil))
153 | ([data schema {:keys [message] :or {message "Schema validation error"} :as opts}]
154 | (let [[errors data] (validate data schema opts)]
155 | (if (seq errors)
156 | (throw (ex-info message errors))
157 | data))))
158 |
159 | (defn valid?
160 | "Return true if the data matches the schema, otherwise
161 | return false."
162 | [data schema]
163 | (nil? (first (validate data schema {::nomsg true}))))
164 |
165 | (defn valid-single?
166 | "Analogous function to `valid?` that just validates single value."
167 | [data schema]
168 | (nil? (first (validate-single data schema {::nomsg true}))))
169 |
170 | ;; --- Validators
171 |
172 | (def keyword
173 | {:message "must be a keyword"
174 | :optional true
175 | :validate keyword?
176 | :coerce identity})
177 |
178 | (def uuid
179 | {:message "must be an uuid"
180 | :optional true
181 | :validate #?(:clj #(instance? java.util.UUID %)
182 | :cljs #(instance? cljs.core.UUID %))})
183 |
184 | (def ^:const ^:private +uuid-re+
185 | #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
186 |
187 | (def uuid-str
188 | {:message "must be an uuid"
189 | :optional true
190 | :validate #(and (string? %)
191 | (re-seq +uuid-re+ %))
192 | :coerce #?(:clj #(java.util.UUID/fromString %)
193 | :cljs #(uuid %))})
194 |
195 | (def email
196 | (let [rx #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"]
197 | {:message "must be a valid email"
198 | :optional true
199 | :validate #(and (string? %)
200 | (re-seq rx %))}))
201 |
202 | (def required
203 | {:message "this field is mandatory"
204 | :optional false
205 | :validate #(if (string? %)
206 | (not (empty? %))
207 | (not (nil? %)))})
208 |
209 | (def number
210 | {:message "must be a number"
211 | :optional true
212 | :validate number?})
213 |
214 | (def number-str
215 | {:message "must be a number"
216 | :optional true
217 | :validate #(or (number? %) (and (string? %) (str/numeric? %)))
218 | :coerce #(if (number? %) % (str/parse-number %))})
219 |
220 | (def integer
221 | {:message "must be a integer"
222 | :optional true
223 | :validate #?(:cljs #(js/Number.isInteger %)
224 | :clj #(integer? %))})
225 |
226 | (def integer-str
227 | {:message "must be a long"
228 | :optional true
229 | :validate #(or (number? %) (and (string? %) (str/numeric? %)))
230 | :coerce #(if (number? %) (int %) (str/parse-int %))})
231 |
232 | (def boolean
233 | {:message "must be a boolean"
234 | :optional true
235 | :validate #(or (= false %) (= true %))})
236 |
237 | (def boolean-str
238 | {:message "must be a boolean"
239 | :optional true
240 | :validate #(and (string? %)
241 | (re-seq #"^(?:t|true|false|f|0|1)$" %))
242 | :coerce #(contains? #{"t" "true" "1"} %)})
243 |
244 | (def string
245 | {:message "must be a string"
246 | :optional true
247 | :validate string?})
248 |
249 | (def string-like
250 | {:message "must be a string"
251 | :optional true
252 | :coerce str})
253 |
254 | (def in-range
255 | {:message "not in range %s and %s"
256 | :optional true
257 | :validate #(and (number? %1)
258 | (number? %2)
259 | (number? %3)
260 | (<= %2 %1 %3))})
261 |
262 | (def positive
263 | {:message "must be positive"
264 | :optional true
265 | :validate pos?})
266 |
267 | (def negative
268 | {:message "must be negative"
269 | :optional true
270 | :validate neg?})
271 |
272 | (def map
273 | {:message "must be a map"
274 | :optional true
275 | :validate map?})
276 |
277 | (def set
278 | {:message "must be a set"
279 | :optional true
280 | :validate set?})
281 |
282 | (def coll
283 | {:message "must be a collection"
284 | :optional true
285 | :validate coll?})
286 |
287 | (def vector
288 | {:message "must be a vector instance"
289 | :optional true
290 | :validate vector?})
291 |
292 | (def every
293 | {:message "must match the predicate"
294 | :optional true
295 | :validate #(every? %2 %1)})
296 |
297 | (def member
298 | {:message "not in coll"
299 | :optional true
300 | :validate #(some #{%1} %2)})
301 |
302 | (def function
303 | {:message "must be a function"
304 | :optional true
305 | :validate ifn?})
306 |
307 | (def identical-to
308 | {:message "does not match"
309 | :optional true
310 | :state true
311 | :validate (fn [state v ref]
312 | (let [prev (get state ref)]
313 | (= prev v)))})
314 |
315 | (def min-count
316 | (letfn [(validate [v minimum]
317 | {:pre [(number? minimum)]}
318 | (>= (count v) minimum))]
319 | {:message "less than the minimum %s"
320 | :optional true
321 | :validate validate}))
322 |
323 | (def max-count
324 | (letfn [(validate [v maximum]
325 | {:pre [(number? maximum)]}
326 | (<= (count v) maximum))]
327 | {:message "longer than the maximum %s"
328 | :optional true
329 | :validate validate}))
330 |
--------------------------------------------------------------------------------
/doc/content.adoc:
--------------------------------------------------------------------------------
1 | = struct - validation library for Clojure(Script)
2 | :toc: left
3 | :!numbered:
4 | :idseparator: -
5 | :idprefix:
6 | :sectlinks:
7 | :source-highlighter: pygments
8 | :pygments-style: friendly
9 |
10 |
11 | == Introduction
12 |
13 | A structural validation library for Clojure and ClojureScript.
14 |
15 | Highlights:
16 |
17 | - *No macros*: validators are defined using plain data.
18 | - *Dependent validators*: the ability to access to already validated data.
19 | - *Coercion*: the ability to coerce incoming values to other types.
20 | - *No exceptions*: no exceptions used in the validation process.
21 |
22 | Based on similar ideas of
23 | link:https://github.com/leonardoborges/bouncer[bouncer].
24 |
25 |
26 | === Project Maturity
27 |
28 | Since _struct_ is a young project there may be some API breakage.
29 |
30 |
31 | === Install
32 |
33 | Just include that in your dependency vector on *_project.clj_*:
34 |
35 | [source,clojure]
36 | ----
37 | [funcool/struct "1.3.0"]
38 | ----
39 |
40 |
41 | == User guide
42 |
43 | === Quick Start
44 |
45 | Let's require the main _struct_ namespace:
46 |
47 | [source, clojure]
48 | ----
49 | (require '[struct.core :as st])
50 | ----
51 |
52 | Define a small schema for the example purpose:
53 |
54 | [source, clojure]
55 | ----
56 | (def +scheme+
57 | {:name [st/required st/string]
58 | :year [st/required st/number]})
59 | ----
60 |
61 | You can observe that it consists in a simple map when you declare keys and
62 | corresponding validators for that key. A vector as value allows us to put
63 | more than one validator for the same key. If you have only one validator for the
64 | key, you can omit the vector and put it as single value.
65 |
66 | The same schema can be defined using vectors, if the order of validation
67 | matters:
68 |
69 | [source, clojure]
70 | ----
71 | (def +scheme+
72 | [[:name st/required st/string]
73 | [:year st/required st/number]])
74 | ----
75 |
76 | By default, all validators are optional so if the value is missing, no error
77 | will reported. If you want make the value mandatory, you should use a specific
78 | `required` validator.
79 |
80 | And finally, start validating your data:
81 |
82 | [source, clojure]
83 | ----
84 | (-> {:name "Blood of Elves" :year 1994}
85 | (st/validate +scheme+))
86 | ;; => [nil {:name "Blood of Elves" :year 1994}]
87 |
88 | (-> {:name "Blood of Elves" :year "1994"}
89 | (st/validate +scheme+))
90 | ;; => [{:year "must be a number"} {:name "Blood of Elves", :year "1994"}]
91 |
92 | (-> {:year "1994"}
93 | (st/validate +scheme+))
94 | ;; => [{:name "this field is mandatory", :year "must be a number"} {}]
95 | ----
96 |
97 | If only want to know if some data is valid or not, you can use the `valid?` predicate
98 | for that purpose:
99 |
100 | [source, clojure]
101 | ----
102 | (st/valid? {:year "1994"} +scheme+)
103 | ;; => false
104 | ----
105 |
106 | The additional entries in the map are not stripped by default, but this behavior
107 | can be changed passing an additional flag as the third argument:
108 |
109 | [source, clojure]
110 | ----
111 | (-> {:name "Blood of Elves" :year 1994 :foo "bar"}
112 | (st/validate +scheme+))
113 | ;; => [nil {:name "Blood of Elves" :year 1994 :foo "bar"}]
114 |
115 | (-> {:name "Blood of Elves" :year 1994 :foo "bar"}
116 | (st/validate +scheme+ {:strip true}))
117 | ;; => [nil {:name "Blood of Elves" :year 1994}]
118 |
119 | ----
120 |
121 | With similar syntax you can validate neested data structures, specifying in the
122 | key part the proper path to the neested data structure:
123 |
124 | [source, clojure]
125 | ----
126 | (def +scheme+
127 | {[:a :b] st/integer
128 | [:c :d] st/string})
129 |
130 | (-> {:a {:b "foo"} {:c {:d "bar"}}}
131 | (st/validate +scheme+))
132 | ;; => [{:a {:b "must be a number"}} {:c {:d "bar"}}]
133 | ----
134 |
135 |
136 | === Parametrized validators
137 |
138 | In addition to simple validators, one may use additional contraints
139 | (e.g. `in-range`). This is how they can be passed to the validator:
140 |
141 | [source, clojure]
142 | ----
143 | (def schema {:num [[st/in-range 10 20]]})
144 |
145 | (st/validate {:num 21} schema)
146 | ;; => [{:num "not in range"} {}]
147 |
148 | (st/validate {:num 19} schema)
149 | ;; => [nil {:num 19}]
150 | ----
151 |
152 | Note the double vector; the outer denotes a list of validatiors and the inner
153 | denotes a validator with patameters.
154 |
155 |
156 | === Custom messages
157 |
158 | The builtin validators comes with default messages in human readable format, but
159 | sometimes you may want to change them (e.g. for i18n purposes). This is how you
160 | can do it:
161 |
162 | [source, clojure]
163 | ----
164 | (def schema
165 | {:num [[st/in-range 10 20 :message "errors.not-in-range"]]})
166 |
167 | (st/validate {:num 21} schema)
168 | ;; => [{:num "errors.not-in-range"} {}]
169 | ----
170 |
171 | A message can contains format wildcards `%s`, these wildcards will be replaced by `args` of validator, e.g.:
172 |
173 | [source, clojure]
174 | ----
175 | (def schema
176 | {:age [[st/in-range 18 26 :message "The age must be between %s and %s"]]})
177 |
178 | (st/validate {:age 30} schema)
179 | ;; => [{:age "The age must be between 18 and 26"} {}]
180 |
181 | ----
182 |
183 |
184 | === Data coercions
185 |
186 | In addition to simple validations, this library includes the ability
187 | to coerce values, and a collection of validators that matches over strings. Let's
188 | see some code:
189 |
190 | .Example attaching custom coercions
191 | [source, clojure]
192 | ----
193 | (def schema
194 | {:year [[st/integer :coerce str]]})
195 |
196 | (st/validate {:year 1994} schema))
197 | ;; => [nil {:year "1994"}]
198 | ----
199 |
200 | Looking at the data returned from the validation
201 | process, one can see that the value is properly coerced with the specified coercion function.
202 |
203 | This library comes with a collection of validators that already
204 | have attached coercion functions. These serve to validate parameters
205 | that arrive as strings but need to be converted to the appropriate type:
206 |
207 | [source, clojure]
208 | ----
209 | (def schema {:year [st/required st/integer-str]
210 | :id [st/required st/uuid-str]})
211 |
212 | (st/validate {:year "1994"
213 | :id "543e7472-6624-4cb5-b65e-f3c341843d0f"}
214 | schema)
215 | ;; => [nil {:year 1994, :id #uuid "543e7472-6624-4cb5-b65e-f3c341843d0f"}]
216 | ----
217 |
218 | To facilitate this operation, the `validate!` function receives the
219 | data and schema, then returns the resulting data. If data not matches the schema
220 | an exception will be raised using `ex-info` clojure facility:
221 |
222 | [source, clojure]
223 | ----
224 | (st/validate! {:year "1994" :id "543e7472-6624-4cb5-b65e-f3c341843d0f"} schema)
225 | ;; => {:year 1994, :id #uuid "543e7472-6624-4cb5-b65e-f3c341843d0f"}
226 | ----
227 |
228 | === Builtin Validators
229 |
230 | This is the table with available builtin validators:
231 |
232 | .Builtin Validators
233 | [options="header", cols="2,1,4"]
234 | |===========================================================================
235 | | Identifier | Coercion | Description
236 | | `struct.core/keyword` | no | Validator for clojure's keyword
237 | | `struct.core/uuid` | no | Validator for UUID's
238 | | `struct.core/uuid-str` | yes | Validator for uuid strings with coercion to UUID
239 | | `struct.core/email` | no | Validator for email string.
240 | | `struct.core/required` | no | Marks field as required.
241 | | `struct.core/number` | no | Validator for Number.
242 | | `struct.core/number-str` | yes | Validator for number string.
243 | | `struct.core/integer` | no | Validator for integer.
244 | | `struct.core/integer-str` | yes | Validator for integer string.
245 | | `struct.core/boolean` | no | Validator for boolean.
246 | | `struct.core/boolean-str` | yes | Validator for boolean string.
247 | | `struct.core/string` | no | Validator for string.
248 | | `struct.core/string-str` | yes | Validator for string like.
249 | | `struct.core/in-range` | no | Validator for a number range.
250 | | `struct.core/member` | no | Validator for check if a value is member of coll.
251 | | `struct.core/positive` | no | Validator for positive number.
252 | | `struct.core/negative` | no | Validator for negative number.
253 | | `struct.core/function` | no | Validator for IFn interface.
254 | | `struct.core/vector` | no | Validator for clojure vector.
255 | | `struct.core/map` | no | Validator for clojure map.
256 | | `struct.core/set` | no | Validator for clojure set.
257 | | `struct.core/coll` | no | Validator for clojure coll.
258 | | `struct.core/every` | no | Validator to check if pred match for every item in coll.
259 | | `struct.core/identical-to` | no | Validator to check that value is identical to other field.
260 | | `struct.core/min-count` | no | Validator to check that value is has at least a minimum number of characters.
261 | | `struct.core/max-count` | no | Validator to check that value is not larger than a maximum number of characters.
262 | |===========================================================================
263 |
264 | Additional notes:
265 |
266 | * `number-str` coerces to `java.lang.Double` or `float` (cljs)
267 | * `boolean-str` coerces to `true` (`"t"`, `"true"`, `"1"`) or `false` (`"f"`, `"false"`, `"0"`).
268 | * `string-str` coerces anything to string using `str` function.
269 |
270 |
271 | === Define your own validator
272 |
273 | As mentioned previously, the validators in _struct_ library are defined using plain
274 | hash-maps. For example, this is how the builtin `integer` validator is defined:
275 |
276 | [source, clojure]
277 | ----
278 | (def integer
279 | {:message "must be a integer"
280 | :optional true
281 | :validate integer?}))
282 | ----
283 |
284 | If the validator needs access to previously validated data, the `:state` key
285 | should be present with the value `true`. Let see the `identical-to` validator as example:
286 |
287 | [source,clojure]
288 | ----
289 | (def identical-to
290 | {:message "does not match"
291 | :optional true
292 | :state true
293 | :validate (fn [state v ref]
294 | (let [prev (get state ref)]
295 | (= prev v)))})
296 | ----
297 |
298 | Validators that access the state receive an additional argument with the state for validator
299 | function.
300 |
301 | === Translating validation messages
302 |
303 | `struct.core/validate` accepts a third options argument where a function can be passed in with the `:translate` key like the following:
304 |
305 | [source,clojure]
306 | ----
307 | (st/validate {:year "1994"
308 | :id "543e7472-6624-4cb5-b65e-f3c341843d0f"}
309 | schema
310 | {:translate (fn [message] (clojure.string/uppercase message)))
311 | ----
312 |
313 | The translation function accepts the `:message` of the schma upon validation failure.
314 | This allows easy integration with an i18n library such as tempura if you privide a keyword for the schema's `:message` that in turn maps to the localised message in the dictionary.
315 |
316 | == Developers Guide
317 |
318 | === Contributing
319 |
320 | Unlike Clojure and other Clojure contrib libs, there aren't many restrictions for
321 | contributions. Just open an issue or pull request.
322 |
323 |
324 | === Get the Code
325 |
326 | _struct_ is open source and can be found on
327 | link:https://github.com/funcool/struct[github].
328 |
329 | You can clone the public repository with this command:
330 |
331 | [source,text]
332 | ----
333 | git clone https://github.com/funcool/struct
334 | ----
335 |
336 |
337 | === Run tests
338 |
339 | To run the tests execute the following:
340 |
341 | For the JVM platform:
342 |
343 | [source, text]
344 | ----
345 | lein test
346 | ----
347 |
348 | And for JS platform:
349 |
350 | [source, text]
351 | ----
352 | ./scripts/build
353 | node out/tests.js
354 | ----
355 |
356 | You will need to have nodejs installed on your system.
357 |
358 | === License
359 |
360 | _struct_ is under public domain:
361 |
362 | ----
363 | This is free and unencumbered software released into the public domain.
364 |
365 | Anyone is free to copy, modify, publish, use, compile, sell, or
366 | distribute this software, either in source code form or as a compiled
367 | binary, for any purpose, commercial or non-commercial, and by any
368 | means.
369 |
370 | In jurisdictions that recognize copyright laws, the author or authors
371 | of this software dedicate any and all copyright interest in the
372 | software to the public domain. We make this dedication for the benefit
373 | of the public at large and to the detriment of our heirs and
374 | successors. We intend this dedication to be an overt act of
375 | relinquishment in perpetuity of all present and future rights to this
376 | software under copyright law.
377 |
378 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
379 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
380 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
381 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
382 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
383 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
384 | OTHER DEALINGS IN THE SOFTWARE.
385 |
386 | For more information, please refer to
387 | ----
388 |
--------------------------------------------------------------------------------