[org.clojure/clojure "1.9.0"]
Much of the essence of building a program is in fact the debugging of the specification.
Specifies the structure of data
Validates data structures
A spec is a single argument function
Values conform to specs or don’t (validation)
A values may conform to one of multiple specs (parsing)
A registrar of named specs
Can generate data and tests
Asserts arbitrary requirements
More flexible than types
Update project.clj
to the right version:
[org.clojure/clojure "1.9.0"]
Require it
(ns training.spec (:require [clojure.spec.alpha :as s]))
(string? 0) => false
(identity 1) => 1
(identity nil) => nil
A truthy result indicates conformity
(s/valid? string? 0) => false
(s/valid? identity nil) => false
(s/valid? identity 1) => true
(s/def ::first-name string?) => :user/first-name
Identifier → spec is stored in the registrar
(s/valid? ::first-name "Tim") => true
::first-name
is shorthand for :my.namespace/first-name
Naming collisions are expected
Spec identifiers must be namespaced
(s/def :corgi-cover/state #{"IL" "WA" "NY" "CO"}) (s/valid? :corgi-cover/state "IL") => true
Sets are functions that return the element if it is in the set |
(s/def ::big-even (s/and int? even? #(> % 1000)))
(s/valid? ::big-even 100000) => true
(s/valid? ::big-even 5) => false
(s/explain ::big-even 5) => val: 5 fails spec: ::big-even predicate: even?
See also explain-str
What would happen if we had not used s/and ? |
#(and (int? %) (even? %) (> % 1000))
(s/def ::name-or-id (s/or :name string? :id int?))
Chose which spec matches
(s/conform ::name-or-id "abc") => [:name "abc"]
(s/conform ::name-or-id 100) => [:id 100]
Each choice is tagged (:name
and :id
)
Events
Function signatures
Expectations about data
Can match one of many alternatives
(string? nil) => false
To include nil
as a valid value:
(s/nilable string?)
(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") (s/def ::email (s/and string? #(re-matches email-regex %)))
(s/valid? ::email "timothypratley@gmail.com") => true (s/valid? ::email "not-a-valid-email-address") => false
(s/def ::first-name string?) (s/def ::last-name string?) (s/def ::phone string?)
(s/def ::person (s/keys :req [::first-name ::last-name] :opt [::phone]))
Entity definition
(s/valid? ::person {::first-name "Elon" ::last-name "Musk" ::email "elon@example.com"}) => true
Required attributes are included
Every registered key has a conforming value
Namespacing keys preserves more meaning
{:my.namespace/first-name "Elon" :my.namespace/last-name "Musk" :my.namespace/email "elon@example.com"}
But existing code often does not namespace keys
{:first-name "Elon" :last-name "Musk" :email "elon@example.com"}
(s/def :unq/person (s/keys :req-un [::first-name ::last-name ::email] :opt-un [::phone]))
(s/valid? :unq/person {:first-name "Elon" :last-name "Musk" :email "elon@example.com"}) => true
req-un → required unqualified keys
opt-un → optional unqualified keys
(s/def ::scores (s/map-of string? int?))
(s/valid? ::scores {"Sally" 1000, "Joe" 500 "Jess" 750}) => true
Homogeneous keys and homogeneous values
(s/valid (s/coll-of number?) #{5 10 2}) => true
(s/valid (s/tuple number? string?) [42 "meaning of life"]) => true
(s/def :mega-corp/name string?) (s/def :mega-corp/policy-count int?) (s/def :corgi-cover/state #{"IL" "WA" "NY" "CO"}) (s/def :corgi-cover/corgi-count pos-int?)
(s/def :mega-corp/corgi-cover (s/keys :req-un [:mega-corp/name :corgi-cover/state :corgi-cover/corgi-count]))
Specs are designed to act as generators
Produce sample data that conforms to the spec
Useful for property-based testing
Add to your project.clj
:
:profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"]]}}
Require:
(ns training.spec (:require [clojure.spec.gen.alpha :as gen]))
(gen/generate (s/gen int?)) => -959
gen
obtains the generator for a spec
generate
creates a value that conforms to the spec
(gen/generate (s/gen :mega-corp/corgi-cover)) => {:name "yNd516AYD", :state "NY", :corgi-count 1}
(gen/sample (s/gen string?)) => ("" "" "" "" "8" "W" "" "G74SmCm" "K9sL9" "82vC")
Produces 10 examples
(s/def ::roll (s/int-in 0 11)) (gen/sample (s/gen ::roll)) => (1 0 0 3 1 7 10 1 5 0)
See also inst-in
and double-in
See also test.check
documentation
(defn f [x y z] ...)
[x y z]
is a sequence of data arguments with different specs
Positional importance
unlike a stream of events |
(s/def ::t (s/cat :a number? :b string?))
(s/conform ::t [2 "three"]) => {:a 2, :b "three"}
Covers most function argument signature
(s/fdef f :args (s/cat ...) :ret ... :fn ...)
Sequence of inputs
Return spec
Invariant function has access to inputs and return
(s/fdef f :args (s/cat :num number? :item string?))
(defn f [num item] (str num " bottles of " item " on the wall"))
Often declared in a different namespace
(s/fdef f :ret string?)
(s/fdef f :fn #(str/includes? (:ret %) (-> % :args :item))
(s/fdef f :args (s/cat :num number? :item string?)) :ret string? :fn #(str/includes? (-> % :args :item) (:ret %))
src/training/core.clj
(ns training.core) (defn f [x] (inc x))
src/training/core_spec.clj
(ns training.core-spec (:require [training.core :as c] [clojure.spec.alpha :as s])) (s/fdef c/f :args (s/cat :x int?) :ret int?)
src/training/core_test
(ns training.core-test (:require [clojure.test :refer :all] [clojure.spec.test.alpha :as stest] [training.core :as c])) (deftest f-test (is (nil? (-> (stest/check `c/f) first :failure))))
Common to keep specs in a separate namespace
stest/check
returns a sequence containing a failure cases
Shrinks to a minimal case
Difficult to find what you want in the output
Designed for running at the REPL, clunky in tests
Make sure you tests for failure before success
The expound library helps
Add to project.clj dependencies
[expound "0.5.0"]
(ns training.expound-test (:require [expound.alpha :as e])) (e/expound string? 1) => -- Spec failed -------------------- 1 should satisfy string? -------------------------
Generates arguments based on the :args
spec
Invokes the function
Checks that :ret
and :fn
specs were satisfied
Reduces to the shortest failing case
(stest/check (stest/enumerate-namespace 'training.core))
Tests all functions in a namespace that have a spec
(stest/check)
Checks all functions that have a spec
(stest/instrument `c/f) (c/f "bad argument") => CompilerException: did not conform to spec
Requires a fully-qualified symbol
Provides validation for external uses of a function
unstrument
to turn off
(stest/instrument (stest/enumerate-namespace 'training.core))
(stest/instrument)
| 0 or more of a pattern |
| 1 or more of a pattern |
| 0 or 1 of a pattern |
(s/valid? (s/* string?) ["a" "b" "c"]) => true
(s/valid? (s/+ string?) []) => false
(s/valid? (s/? string?) ["a" "b"]) => false
(s/def ::t (s/cat :a (s/* int?) :b string?))
(s/conform ::t [1 2 "three"]) => {:a [1 2], :b "three"}
Matched [1 2 "three"] , not [[1 2] "three"] |
Think of it as a regex: int*string
"Any number of ints followed by a string"
(s/def ::t (s/cat :a (s/alt :b (s/* int?) :c (s/* string?)) :d keyword?))
Builds a regex: (int*|string*)keyword
[1 2 3 :foo] ["abc" "def" :bar]
Is s/alt the same as s/or ? |
Inside a sequence s/or
would match
[[1 2 3] :foo] [["abc" "def"] :bar]
The key is to think about the regex being constructed
Outside of sequences they do behave the same
(s/def ::even-strings (s/cat :a (s/& (s/* string?) #(even? (count %))) :b keyword?))
Matches
["hello" "world" :k]
s/&
is like s/and
s/&
is can participate in subsequences (similar to s/alt
vs s/or
)
(s/exercise (s/* (s/cat :w (s/alt :x int?))))
(s/exercise-fn training.core/f)
Generates 10 samples and conforms them
Very useful to test specs as you build them
Can see the example and where the parts were matched
Spec data
Validate data
Spec function arguments
Generate data from specs
Check functions with generated data
Instrument functions