Skip to content

Instantly share code, notes, and snippets.

@bobbicodes
Last active October 22, 2018 20:11
Show Gist options
  • Save bobbicodes/00d83fa71b57bc7cbefd8f3cb6cab107 to your computer and use it in GitHub Desktop.
Save bobbicodes/00d83fa71b57bc7cbefd8f3cb6cab107 to your computer and use it in GitHub Desktop.
;; 100 Days of Clojure Code
;; Day 37: October 21, 2018
;; Interactive Clojure Spec Guide
;; This is a rewrite of the official guide on clojure.org.
;; Edit code blocks and evaluate them with Ctrl+Enter or Command+Return. Even use as a project template!
;; ## Getting started
;; The [spec](https://clojure.org/about/spec) library specifies the structure of data, validates or destructures it, and can generate data based on the spec.
;; To start working with spec, require the `clojure.spec.alpha`namespace:
(ns porkostomus.log
(:require [clojure.spec.alpha :as s]))
;; #### Predicates
;; Each spec describes a set of allowed values. There are several ways to build specs and all of them can be composed to build more sophisticated specs.
;; Any existing Clojure function that takes a single argument and returns a truthy value is a valid predicate spec. We can check whether a particular data value conforms to a spec using [`conform`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/conform):
(s/conform even? 1000)
;; The `conform` function takes something that can be a spec and a data value. Here we are passing a predicate which is implicitly converted into a spec. The return value is "conformed". Here, the conformed value is the same as the original value - we’ll see later where that starts to deviate. If the value does not conform to the spec, the special value `:clojure.spec.alpha/invalid` is returned.
;; If you don’t want to use the conformed value or check for `:clojure.spec.alpha/invalid`, the helper [`valid?`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/valid?) can be used instead to return a boolean.
(s/valid? even? 10)
;; Note that again `valid?` implicitly converts the predicate function into a spec. The spec library allows you to leverage all of the functions you already have - there is no special dictionary of predicates. Some more examples:
(s/valid? nil? nil)
(s/valid? string? "abc")(s/valid? #(> % 5) 10) (s/valid? #(> % 5) 0)
;; Sets can also be used as predicates that match one or more literal values:
(s/valid? #{:club :diamond :heart :spade} :club)
(s/valid? #{:club :diamond :heart :spade} 42)
(s/valid? #{42} 42)
;; Registry
;; Until now, we’ve been using specs directly. However, spec provides a central registry for globally declaring reusable specs. The registry associates a namespaced keyword with a specification. The use of namespaces ensures that we can define reusable non-conflicting specs across libraries or applications.
;; Specs are registered using [`def`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/def). It’s up to you to register the specification in a namespace that makes sense (typically a namespace you control).
(s/def ::date #(instance? js/Date %))
(s/def ::suit #{:club :diamond :heart :spade})
;; A registered spec identifier can be used in place of a spec definition in the operations we’ve seen so far - `conform` and `valid?`.
(s/valid? ::date (js/Date.))
(s/conform ::suit :club)
;; You will see later that registered specs can (and should) be used anywhere we compose specs.
;; Spec Names
;; In this guide we will often use auto-resolved keywords like `::date`. The Clojure reader resolves these to a fully-qualified keyword using the current namespace. You may also see some cases where a fully-qualified keyword like `:animal/dog` is used to name a spec.
;; Generally, Clojure code should use keyword namespaces that are sufficiently unique such that they will not conflict with other spec users. If you are writing a library for public use, spec namespaces should include the project name, url, or organization such that you will not conflict. Within a private organization, you may be able to use shorter names - the important thing is that they are sufficiently unique to avoid conflicts.
;; ## Composing predicates
;; The simplest way to compose specs is with [`and`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/and) and [`or`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/or). Let’s create a spec that combines several predicates into a composite spec with `s/and`:
(s/def ::big-even (s/and int? even? #(> % 1000)))
(s/valid? ::big-even :foo)
(s/valid? ::big-even 10)
(s/valid? ::big-even 100000)
;; We can also use `s/or` to specify two alternatives:
(s/def ::name-or-id (s/or :name string?
:id int?))
(s/valid? ::name-or-id "abc")
(s/valid? ::name-or-id 100)
(s/valid? ::name-or-id :foo)
;; This `or` spec is the first case we’ve seen that involves a choice during validity checking. Each choice is annotated with a tag (here, between `:name` and `:id`) and those tags give the branches names that can be used to understand or enrich the data returned from `conform` and other spec functions.
;; When an `or` is conformed, it returns a vector with the tag name and conformed value:
(s/conform ::name-or-id "abc")
(s/conform ::name-or-id 100)
;; Many predicates that check an instance’s type do not allow `nil` as a valid value (`string?`, `number?`, `keyword?`, etc). To include `nil` as a valid value, use the provided function [`nilable`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/nilable) to make a spec:
(s/valid? string? nil)
(s/valid? (s/nilable string?) nil)
;; ## Explain
;; [explain](http://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/explain), [explain-str](http://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/explain-str) and [explain-data](http://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/explain-data) are another high-level operations in `spec` that can be used to report why a value does not conform to a spec. Let’s see it in action with some non-conforming examples we’ve seen so far:
(with-out-str (s/explain ::suit 42))
(s/explain-data ::big-even 5)(s/explain-str ::name-or-id :foo)
;; Let’s examine the output of the final example more closely. First note that there are two errors being reported - spec will evaluate all possible alternatives and report errors on every path. The parts of each error are:
;; * val - the value in the user’s input that does not match
;; * spec - the spec that was being evaluated
;; * at - a path (a vector of keywords) indicating the location within the spec where the error occurred - the tags in the path correspond to any tagged part in a spec (the alternatives in an `or` or `alt`, the parts of a `cat`, the keys in a map, etc)
;; * predicate - the actual predicate that was not satisfied by val
;; * in - the key path through a nested data val to the failing value. In this example, the top-level value is the one that is failing so this is essentially an empty path and is omitted.
;; For the first reported error we can see that the value `:foo` did not satisfy the predicate `string?` at the path `:name` in the spec `::name-or-id`. The second reported error is similar but fails on the `:id` path instead. The actual value is a keyword so neither is a match.
;; In addition to `explain`, you can use [`explain-str`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/explain-str) to receive the error messages as a string or [`explain-data`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/explain-data) to receive the errors as data.
(s/explain-data ::name-or-id :foo)
;; This result also demonstrates the new namespace map literal syntax added in Clojure 1.9. Maps may be prefixed with `#:` or `#::` (for autoresolve) to specify a default namespace for all keys in the map. In this example, this is equivalent to `{:clojure.spec.alpha/problems …​}`
;; ## Entity Maps
;; Clojure programs rely heavily on passing around maps of data. A common approach in other libraries is to describe each entity type, combining both the keys it contains and the structure of their values. Rather than define attribute (key+value) specifications in the scope of the entity (the map), specs assign meaning to individual attributes, then collect them into maps using set semantics (on the keys). This approach allows us to start assigning (and sharing) semantics at the attribute level across our libraries and applications.
;; For example, most Ring middleware functions modify the request or response map with unqualified keys. However, each middleware could instead use namespaced keys with registered semantics for those keys. The keys could then be checked for conformance, creating a system with greater opportunities for collaboration and consistency.
;; Entity maps in spec are defined with [`keys`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/keys):
(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
(s/def ::email-type (s/and string? #(re-matches email-regex %))) (s/def ::acctid int?)
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::email ::email-type)
(s/def ::person (s/keys :req [::first-name ::last-name ::email] :opt [::phone]))
;; This registers a `::person` spec with the required keys `::first-name`, `::last-name`, and `::email`, with optional key `::phone`. The map spec never specifies the value spec for the attributes, only what attributes are required or optional.
;; When conformance is checked on a map, it does two things - checking that the required attributes are included, and checking that every registered key has a conforming value. We’ll see later where optional attributes can be useful. Also note that ALL attributes are checked via `keys`, not just those listed in the `:req` and `:opt` keys. Thus a bare `(s/keys)` is valid and will check all attributes of a map without checking which keys are required or optional.
(s/valid? ::person {::first-name "Elon" ::last-name "Musk" ::email "elon@example.com"})
;; Fails required key check
(with-out-str (s/explain ::person {::first-name "Elon"}))
;; Fails attribute conformance
(with-out-str
(s/explain ::person
{::first-name "Elon"
::last-name "Musk"
::email "n/a"}))
;; In: [:my.domain/email] val: "n/a" fails spec: :my.domain/email-type
;; at: [:my.domain/email] predicate: (re-matches email-regex %)
;; Let’s take a moment to examine the explain error output on that final example:
;; * in - the path within the data to the failing value (here, a key in the person instance)
;; * val - the failing value, here `"n/a"`
;; * spec - the spec that failed, here `:my.domain/email-type`
;; * at - the path in the spec where the failing value is located
;; * predicate - the predicate that failed, here `(re-matches email-regex %)`
;; Much existing Clojure code does not use maps with namespaced keys and so `keys` can also specify `:req-un` and `:opt-un` for required and optional unqualified keys. These variants specify namespaced keys used to find their specification, but the map only checks for the unqualified version of the keys.
;; Let’s consider a person map that uses unqualified keys but checks conformance against the namespaced specs we registered earlier:
(s/def :unq/person
(s/keys :req-un [::first-name ::last-name ::email]
:opt-un [::phone]))
(s/conform :unq/person
{:first-name "Elon"
:last-name "Musk"
:email "elon@example.com"})
(with-out-str
(s/explain :unq/person
{:first-name "Elon"
:last-name "Musk"
:email "n/a"}))
;; In: [:email] val: "n/a" fails spec: :my.domain/email-type at: [:email]
;; predicate: (re-matches email-regex %)
(with-out-str
(s/explain :unq/person
{:first-name "Elon"}))
;; Unqualified keys can also be used to validate record attributes:
(defrecord Person [first-name last-name email phone])
(with-out-str
(s/explain :unq/person
(->Person "Elon" nil nil nil)))
;; In: [:last-name] val: nil fails spec: :my.domain/last-name at: [:last-name] predicate: string? ;; In: [:email] val: nil fails spec: :my.domain/email-type at: [:email] predicate: string?
(s/conform :unq/person
(->Person "Elon" "Musk" "elon@example.com" nil))
;; One common occurrence in Clojure is the use of "keyword args" where keyword keys and values are passed in a sequential data structure as options. Spec provides special support for this pattern with the regex op [`keys*`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/keys\*). `keys*` has the same syntax and semantics as `keys` but can be embedded inside a sequential regex structure.
(s/def ::port number?)
(s/def ::host string?)
(s/def ::id keyword?)
(s/def ::server (s/keys* :req [::id ::host] :opt [::port]))
(s/conform ::server [::id :s1 ::host "example.com" ::port 5555])
;; Sometimes it will be convenient to declare entity maps in parts, either because there are different sources for requirements on an entity map or because there is a common set of keys and variant-specific parts. The `s/merge` spec can be used to combine multiple `s/keys` specs into a single spec that combines their requirements. For example consider two `keys` specs that define common animal attributes and some dog-specific ones. The dog entity itself can be described as a `merge` of those two attribute sets:
(s/def :animal/kind string?)
(s/def :animal/says string?)
(s/def :animal/common (s/keys :req [:animal/kind :animal/says]))
(s/def :dog/tail? boolean?)
(s/def :dog/breed string?)
(s/def :animal/dog (s/merge :animal/common
(s/keys :req [:dog/tail? :dog/breed])))
(s/valid? :animal/dog
{:animal/kind "dog"
:animal/says "woof"
:dog/tail? true
:dog/breed "retriever"})
;; ## multi-spec
;; One common occurrence in Clojure is to use maps as tagged entities and a special field that indicates the "type" of the map where type indicates a potentially open set of types, often with shared attributes across the types.
;; As previously discussed, the attributes for all types are well-specified using attributes stored in the registry by namespaced keyword. Attributes shared across entity types automatically gain shared semantics. However, we also want to be able to specify the required keys per entity type and for that spec provides [`multi-spec`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/multi-spec) which leverages a multimethod to provide for the specification of an open set of entity types based on a type tag.
;; For example, imagine an API that received event objects which shared some common fields but also had type-specific shapes. First we would register the event attributes:
(s/def :event/type keyword?)
(s/def :event/timestamp int?)
(s/def :search/url string?)
(s/def :error/message string?)
(s/def :error/code int?)
;; We then need a multimethod that defines a dispatch function for choosing the selector (here our `:event/type` field) and returns the appropriate spec based on the value:
(defmulti event-type :event/type)
(defmethod event-type :event/search [_] (s/keys :req [:event/type :event/timestamp :search/url]))
(defmethod event-type :event/error [_] (s/keys :req [:event/type :event/timestamp :error/message :error/code]))
;; The methods should ignore their argument and return the spec for the specified type. Here we’ve fully spec’ed two possible events - a "search" event and an "error" event.
;; And then finally we are ready to declare our `multi-spec` and try it out.
(s/def :event/event (s/multi-spec event-type :event/type))
(s/valid? :event/event
{:event/type :event/search
:event/timestamp 1463970123000
:search/url "https://clojure.org"})
(s/valid? :event/event
{:event/type :event/error
:event/timestamp 1463970123000
:error/message "Invalid host"
:error/code 500})
(with-out-str
(s/explain :event/event
{:event/type :event/restart}))
(with-out-str
(s/explain :event/event
{:event/type :event/search
:search/url 200}))
;; Let’s take a moment to examine the explain error output on that final example. There were two different kinds of failures detected. The first failure is due to the missing required `:event/timestamp` key in the event. The second is from the invalid `:search/url` value (a number instead of a string). We see the same parts as prior explain errors:
;; * in - the path within the data to the failing value. This is omitted on the first error as it’s at the root value but is the key in the map on the second error.
;; * val - the failing value, either the full map or the individual key in the map
;; * spec - the actual spec that failed
;; * at - the path in the spec where the failing value occurred
;; * predicate - the actual predicate that failed
;; The `multi-spec` approach allows us to create an **open** system for spec validation, just like multimethods and protocols. New event types can be added later by just extending the `event-type` multimethod.
;; ## Collections
;; A few helpers are provided for other special collection cases - [`coll-of`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/coll-of), [`tuple`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/tuple), and [`map-of`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/map-of).
;; For the special case of a homogenous collection of arbitrary size, you can use `coll-of` to specify a collection of elements satisfying a predicate.
(s/conform (s/coll-of keyword?) [:a :b :c])
(s/conform (s/coll-of number?) #{5 10 2})
;; Additionally, `coll-of` can be passed a number of keyword arg options:
;; * `:kind` - a predicate or spec that the incoming collection must satisfy, such as `vector?`
;; * `:count` - specifies exact expected count
;; * `:min-count`, `:max-count` - checks that collection has `(<= min-count count max-count)`
;; * `:distinct` - checks that all elements are distinct
;; * `:into` - one of \[\], (), {}, or #{} for output conformed value. If `:into` is not specified, the input collection type will be used.
;; Following is an example utilizing some of these options to spec a vector containing three distinct numbers conformed as a set and some of the errors for different kinds of invalid values:
(s/def ::vnum3 (s/coll-of number? :kind vector? :count 3 :distinct true :into #{}))
(s/conform ::vnum3 [1 2 3])
(with-out-str (s/explain ::vnum3 #{1 2 3})) (with-out-str (s/explain ::vnum3 [1 1 1])) (with-out-str (s/explain ::vnum3 [1 2 :a]))
;; Both `coll-of` and `map-of` will conform all of their elements, which may make them unsuitable for large collections. In that case, consider [`every`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/every) or for maps[`every-kv`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/every-kv).
;; While `coll-of` is good for homogenous collections of any size, another case is a fixed-size positional collection with fields of known type at different positions. For that we have `tuple`.
(s/def ::point (s/tuple double? double? double?))
(s/conform ::point [1.5 2.5 -0.5])
;; Note that in this case of a "point" structure with x/y/z values we actually had a choice of three possible specs:
;; * Regular expression - `(s/cat :x double? :y double? :z double?)`
;; * Allows for matching nested structure (not needed here)
;; * Conforms to map with named keys based on the `cat` tags
;; * Collection - `(s/coll-of double?)`
;; * Designed for arbitrary size homogenous collections
;; * Conforms to a vector of the values
;; * Tuple - `(s/tuple double? double? double?)`
;; * Designed for fixed size with known positional "fields"
;; * Conforms to a vector of the values
;; In this example, `coll-of` will match other (invalid) values as well (like `[1.0]` or `[1.0 2.0 3.0 4.0])`, so it is not a suitable choice - we want fixed fields. The choice between a regular expression and tuple here is to some degree a matter of taste, possibly informed by whether you expect either the tagged return values or error output to be better with one or the other.
;; In addition to the support for information maps via `keys`, spec also provides `map-of` for maps with homogenous key and value predicates.
(s/def ::scores (s/map-of string? int?))
(s/conform ::scores {"Sally" 1000, "Joe" 500})
;; By default `map-of` will validate but not conform keys because conformed keys might create key duplicates that would cause entries in the map to be overridden. If conformed keys are desired, pass the option `:conform-keys true`.
;; You can also use the various count-related options on `map-of` that you have with `coll-of`.
;; ## Sequences
;; Sometimes sequential data is used to encode additional structure (typically new syntax, often used in macros). spec provides the standard [regular expression](https://en.wikipedia.org/wiki/Regular_expression) operators to describe the structure of a sequential data value:
;; * [`cat`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/cat) - concatenation of predicates/patterns
;; * [`alt`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/alt) - choice among alternative predicates/patterns
;; * [`*`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/\*) - 0 or more of a predicate/pattern
;; * [`+`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/%2B) - 1 or more of a predicate/pattern
;; * [`?`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/%3F) - 0 or 1 of a predicate/pattern
;; Like `or`, both `cat` and `alt` tag their "parts" - these tags are then used in the conformed value to identify what was matched, to report errors, and more.
;; Consider an ingredient represented by a vector containing a quantity (number) and a unit (keyword). The spec for this data uses `cat` to specify the right components in the right order. Like predicates, regex operators are implicitly converted to specs when passed to functions like `conform`, `valid?`, etc.
(s/def ::ingredient (s/cat :quantity number? :unit keyword?))
(s/conform ::ingredient [2 :teaspoon])
;; The data is conformed as a map with the tags as keys. We can use `explain` to examine non-conforming data.
;; pass string for unit instead of keyword
(with-out-str (s/explain ::ingredient [11 "peaches"]))
;; leave out the unit
(with-out-str (s/explain ::ingredient [2]))
;; Let’s now see the various occurrence operators `*`, `+`, and `?`:
(s/def ::seq-of-keywords (s/* keyword?))
(s/conform ::seq-of-keywords [:a :b :c])
(with-out-str (s/explain ::seq-of-keywords [10 20]))
(s/def ::odds-then-maybe-even (s/cat :odds (s/+ odd?)
:even (s/? even?)))
(s/conform ::odds-then-maybe-even [1 3 5 100])
(s/conform ::odds-then-maybe-even [1])
(with-out-str (s/explain ::odds-then-maybe-even [100]))
;; opts are alternating keywords and booleans
(s/def ::opts (s/* (s/cat :opt keyword? :val boolean?)))
(s/conform ::opts [:silent? false :verbose true])
;; Finally, we can use `alt` to specify alternatives within the sequential data. Like `cat`, `alt`requires you to tag each alternative but the conformed data is a vector of tag and value.
(s/def ::config (s/*
(s/cat :prop string?
:val (s/alt :s string? :b boolean?))))
(s/conform ::config ["-server" "foo" "-verbose" true "-user" "joe"])
;; If you need a description of a specification, use `describe` to retrieve one. Let’s try it on some of the specifications we’ve already defined:
(s/describe ::seq-of-keywords)
(s/describe ::odds-then-maybe-even)
(s/describe ::opts)
;; Spec also defines one additional regex operator, [`&`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/&), which takes a regex operator and constrains it with one or more additional predicates. This can be used to create regular expressions with additional constraints that would otherwise require custom predicates. For example, consider wanting to match only sequences with an even number of strings:
(s/def ::even-strings (s/& (s/* string?) #(even? (count %))))
(s/valid? ::even-strings ["a"])
(s/valid? ::even-strings ["a" "b"])
(s/valid? ::even-strings ["a" "b" "c"])
(s/valid? ::even-strings ["a" "b" "c" "d"])
;; When regex ops are combined, they describe a single sequence. If you need to spec a nested sequential collection, you must use an explicit call to [`spec`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/spec) to start a new nested regex context. For example to describe a sequence like `[:names ["a" "b"] :nums [1 2 3]]`, you need nested regular expressions to describe the inner sequential data:
(s/def ::nested
(s/cat :names-kw #{:names}
:names (s/spec (s/* string?))
:nums-kw #{:nums}
:nums (s/spec (s/* number?))))
(s/conform ::nested [:names ["a" "b"] :nums [1 2 3]])
;; If the specs were removed this spec would instead match a sequence like `[:names "a" "b" :nums 1 2 3]`.
(s/def ::unnested
(s/cat :names-kw #{:names}
:names (s/* string?)
:nums-kw #{:nums}
:nums (s/* number?)))
(s/conform ::unnested [:names "a" "b" :nums 1 2 3])
;; ## Using spec for validation
;; Now is a good time to step back and think about how spec can be used for runtime data validation.
;; One way to use spec is to explicitly call `valid?` to verify input data passed to a function. You can, for example, use the existing pre- and post-condition support built into `defn`:
(defn person-name
[person]
{:pre [(s/valid? ::person person)]
:post [(s/valid? string? %)]}
(str (::first-name person) " " (::last-name person)))
(person-name 42)
(person-name {::first-name "Elon" ::last-name "Musk" ::email "elon@example.com"})
;; When the function is invoked with something that isn’t valid `::person` data, the pre-condition fails. Similarly, if there was a bug in our code and the output was not a string, the post-condition would fail.
;; Another option is to use `s/assert` within your code to assert that a value satisfies a spec. On success the value is returned and on failure an assertion error is thrown. By default assertion checking is off - this can be changed at the REPL with `s/check-asserts` or on startup by setting the system property `clojure.spec.check-asserts=true`.
(defn person-name
[person]
(let [p (s/assert ::person person)]
(str (::first-name p) " " (::last-name p))))
(s/check-asserts true)
(person-name 100)
;; CompilerException clojure.lang.ExceptionInfo: Spec assertion failed
;; val: 100 fails predicate: map?
;; :clojure.spec.alpha/failure :assertion-failed
;; #:clojure.spec.alpha{:problems [{:path [], :pred map?, :val 100, :via [], :in []}],
;; :failure :assertion-failed}
;; A deeper level of integration is to call conform and use the return value to destructure the input. This will be particularly useful for complex inputs with alternate options.
;; Here we conform using the config specification defined above:
(defn- set-config [prop val]
;; dummy fn
(println "set" prop val))
(defn configure [input]
(let [parsed (s/conform ::config input)]
(if (= parsed ::s/invalid)
(throw (ex-info "Invalid input" (s/explain-data ::config input)))
(for [{prop :prop [_ val] :val} parsed]
(set-config (subs prop 1) val)))))
(configure ["-server" "foo" "-verbose" true "-user" "joe"])
;; Here configure calls `conform` to destructure the config input. The result is either the special `::s/invalid` value or a destructured form of the result:
[{:prop "-server", :val [:s "foo"]}
{:prop "-verbose", :val [:b true]}
{:prop "-user", :val [:s "joe"]}]
;; In the success case, the parsed input is transformed into the desired shape for further processing. In the error case, we call `explain-data` to generate error message data. The explain data contains information about what expression failed to conform, the path to that expression in the specification, and the predicate it was attempting to match.
;; ## Spec’ing functions
;; The pre- and post-condition example in the previous section hinted at an interesting question - how do we define the input and output specifications for a function or macro?
;; Spec has explicit support for this using [`fdef`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/fdef), which defines specifications for a function - the arguments and/or the return value spec, and optionally a function that can specify a relationship between args and return.
;; Let’s consider a `ranged-rand` function that produces a random number in a range:
(defn ranged-rand
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- end start)))))
;; We can then provide a specification for that function:
(s/fdef ranged-rand
:args (s/and (s/cat :start int? :end int?)
#(< (:start %) (:end %)))
:ret int?
:fn (s/and #(>= (:ret %) (-> % :args :start))
#(< (:ret %) (-> % :args :end))))
;; This function spec demonstrates a number of features. First the `:args` is a compound spec that describes the function arguments. This spec is invoked with the args in a list, as if they were passed to `(apply fn (arg-list))`. Because the args are sequential and the args are positional fields, they are almost always described using a regex op, like `cat`, `alt`, or `*`.
;; The second `:args` predicate takes as input the conformed result of the first predicate and verifies that start < end. The `:ret` spec indicates the return is also an integer. Finally, the `:fn` spec checks that the return value is >= start and < end.
;; Once a spec has been created for a function, the `doc` for the function will also include it:
(doc ranged-rand)
-------------------------
user/ranged-rand
([start end])
Returns random int in range start <= rand < end
Spec
args: (and (cat :start int? :end int?) (< (:start %) (:end %)))
ret: int?
fn: (and (>= (:ret %) (-> % :args :start)) (< (:ret %) (-> % :args :end)))
;; We’ll see later how we can use a function spec for development and testing.
;; ## Higher order functions
;; Higher order functions are common in Clojure and spec provides [`fspec`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/fspec) to support spec’ing them.
;; For example, consider the `adder` function:
(defn adder [x] #(+ x %))
;; `adder` returns a function that adds x. We can declare a function spec for `adder` using `fspec` for the return value:
(s/fdef adder
:args (s/cat :x number?)
:ret (s/fspec :args (s/cat :y number?)
:ret number?)
:fn #(= (-> % :args :x) ((:ret %) 0)))
;; The `:ret` spec uses `fspec` to declare that the returning function takes and returns a number. Even more interesting, the `:fn` spec can state a general property that relates the `:args` (where we know x) and the result we get from invoking the function returned from `adder`, namely that adding 0 to it should return x.
;; ## Macros
;; As macros are functions that take code and produce code, they can also be spec’ed like functions. One special consideration however is that you must keep in mind that you are receiving code as data, not evaluated arguments, and that you are most commonly producing new code as data, so often it’s not helpful to spec the :ret value of a macro (as it’s just code).
;; For example, we could spec the `clojure.core/declare` macro like this:
(s/fdef clojure.core/declare
:args (s/cat :names (s/* simple-symbol?))
:ret any?)
;; The Clojure macroexpander will look for and conform :args specs registered for macros at macro expansion time (not runtime!). If an error is detected, `explain` will be invoked to explain the error:
(declare 100)
;; ExceptionInfo: Call to clojure.core/declare did not conform to spec:
;; In: [0] val: 100 fails at: [:args :names] predicate: simple-symbol?
;; :clojure.spec.alpha/args (100)
;; Because macros are always checked during macro expansion, you do not need to call instrument for macro specs.
;; ## A game of cards
;; Here’s a bigger set of specs to model a game of cards:
(def suit? #{:club :diamond :heart :spade})
(def rank? (into #{:jack :queen :king :ace} (range 2 11)))
(def deck (for [suit suit? rank rank?] [rank suit]))
(s/def ::card (s/tuple rank? suit?))
(s/def ::hand (s/* ::card))
(s/def ::name string?)
(s/def ::score int?)
(s/def ::player (s/keys :req [::name ::score ::hand]))
(s/def ::players (s/* ::player))
(s/def ::deck (s/* ::card))
(s/def ::game (s/keys :req [::players ::deck]))
;; We can validate a piece of this data against the schema:
(def kenny
{::name "Kenny Rogers"
::score 100
::hand []})
(s/valid? ::player kenny)
;; Or look at the errors we’ll get from some bad data:
(with-out-str (s/explain ::game
{::deck deck
::players [{::name "Kenny Rogers"
::score 100
::hand [[2 :banana]]}]}))
;; The error indicates the key path in the data structure down to the invalid value, the non-matching value, the spec part it’s trying to match, the path in that spec, and the predicate that failed.
;; If we have a function `deal` that doles out some cards to the players we can spec that function to verify the arg and return value are both suitable data values. We can also specify a `:fn` spec to verify that the count of cards in the game before the deal equals the count of cards after the deal.
(defn total-cards [{:keys [::deck ::players] :as game}]
(apply + (count deck)
(map #(-> % ::hand count) players)))
(defn deal [game] ....)
(s/fdef deal
:args (s/cat :game ::game)
:ret ::game
:fn #(= (total-cards (-> % :args :game))
(total-cards (-> % :ret))))
;; ## Generators
;; A key design constraint of spec is that all specs are also designed to act as generators of sample data that conforms to the spec (a critical requirement for property-based testing).
;; ### Project Setup
;; spec generators rely on the Clojure property testing library [test.check](https://github.com/clojure/test.check). However, this dependency is dynamically loaded and you can use the parts of spec other than `gen`, `exercise`, and testing without declaring test.check as a runtime dependency. When you wish to use these parts of spec (typically during testing), you will need to declare a dev dependency on test.check.
;; In Leiningen add this to project.clj:
;; ```
;; :profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"]]}}
;; ```
;; In Leiningen the dev profile dependencies are included during testing but not published as a dependency or included in uber jars.
;; In Boot, add your dependency with test scope in your build.boot file (this is also possible in Leiningen but the approach above is preferred):
;; ```
;; (set-env! :dependencies '[[org.clojure/test.check "0.9.0" :scope "test"]])
;; ```
;; In Maven, declare your dependency as a test scope dependency:
;; ```
;; <project> ... <dependencies> <dependency> <groupId>org.clojure</groupId> <artifactId>test.check</artifactId> <version>0.9.0</version> <scope>test</scope> </dependency> </dependency> </project>
;; ```
;; In your code you also need to include the `clojure.spec.gen.alpha` namespace:
(ns porkostomus.log
(:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]))
;; ### Sampling Generators
;; The [`gen`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/gen) function can be used to obtain the generator for any spec.
;; Once you have obtained a generator with `gen`, there are several ways to use it. You can generate a single sample value with [`generate`](https://clojure.github.io/spec.alpha/clojure.spec.gen.alpha-api.html#clojure.spec.gen.alpha/generate) or a series of samples with [`sample`](https://clojure.github.io/spec.alpha/clojure.spec.gen.alpha-api.html#clojure.spec.gen.alpha/sample). Let’s see some basic examples:
(gen/generate (s/gen int?))
(gen/generate (s/gen nil?))
(gen/sample (s/gen string?))
(gen/sample (s/gen #{:club :diamond :heart :spade}))
(gen/sample (s/gen (s/cat :k keyword? :ns (s/+ number?))))
;; What about generating a random player in our card game?
(gen/generate (s/gen ::player))
;; What about generating a whole game?
(gen/generate (s/gen ::game))
;; So we can now start with a spec, extract a generator, and generate some data. All generated data will conform to the spec we used as a generator. For specs that have a conformed value different than the original value (anything using s/or, s/cat, s/alt, etc) it can be useful to see a set of generated samples plus the result of conforming that sample data.
;; ### Exercise
;; For this we have [`exercise`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/exercise), which returns pairs of generated and conformed values for a spec. `exercise` by default produces 10 samples (like `sample`) but you can pass both functions a number indicating the number of samples to produce.
(s/exercise (s/cat :k keyword? :ns (s/+ number?)) 5)
(s/exercise (s/or :k keyword? :s string? :n number?) 5)
;; For spec’ed functions we also have [`exercise-fn`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/exercise-fn), which generates sample args, invokes the spec’ed function and returns the args and the return value.
(s/exercise-fn `ranged-rand)
;; ### Using `s/and` Generators
;; All of the generators we’ve seen worked fine but there are a number of cases where they will need some additional help. One common case is when the predicate implicitly presumes values of a particular type but the spec does not specify them:
(gen/generate (s/gen even?))
;; In this case spec was not able to find a generator for the `even?` predicate. Most of the primitive generators in spec are mapped to the common type predicates (strings, numbers, keywords, etc).
;; However, spec is designed to support this case via `and` - the first predicate will determine the generator and subsequent branches will act as filters by applying the predicate to the produced values (using test.check’s `such-that`).
;; If we modify our predicate to use an `and` and a predicate with a mapped generator, the `even?` can be used as a filter for generated values instead:
(gen/generate (s/gen (s/and int? even?)))
;; We can use many predicates to further refine the generated values. For example, say we only wanted to generate numbers that were positive multiples of 3:
(defn divisible-by [n] #(zero? (mod % n)))
(gen/sample (s/gen (s/and int?
#(> % 0)
(divisible-by 3))))
;; However, it is possible to go too far with refinement and make something that fails to produce any values. The test.check [`such-that`](https://clojure.github.io/test.check/clojure.test.check.generators.html#var-such-that) that implements the refinement will throw an error if the refinement predicate cannot be resolved within a relatively small number of attempts. For example, consider trying to generate strings that happen to contain the word "hello":
;; hello, are you the one I'm looking for?
(gen/sample (s/gen (s/and string? #(clojure.string/includes? % "hello"))))
;; Given enough time (maybe a lot of time), the generator probably would come up with a string like this, but the underlying `such-that` will make only 100 attempts to generate a value that passes the filter. This is a case where you will need to step in and provide a custom generator.
;; ### Custom Generators
;; Building your own generator gives you the freedom to be either narrower and/or be more explicit about what values you want to generate. Alternately, custom generators can be used in cases where conformant values can be generated more efficiently than using a base predicate plus filtering. Spec does not trust custom generators and any values they produce will also be checked by their associated spec to guarantee they pass conformance.
;; There are three ways to build up custom generators - in decreasing order of preference:
;; 1. Let spec create a generator based on a predicate/spec
;; 2. Create your own generator from the tools in clojure.spec.gen.alpha
;; 3. Use test.check or other test.check compatible libraries (like [test.chuck](https://github.com/gfredericks/test.chuck))
;; The last option requires a runtime dependency on test.check so the first two options are strongly preferred over using test.check directly.
;; First consider a spec with a predicate to specify keywords from a particular namespace:
(s/def ::kws (s/and keyword? #(= (namespace %) "my.domain")))
(s/valid? ::kws :my.domain/name)
(gen/sample (s/gen ::kws))
;; unlikely we'll generate useful keywords this way
;; The simplest way to start generating values for this spec is to have spec create a generator from a fixed set of options. A set is a valid predicate spec so we can create one and ask for it’s generator:
(def kw-gen (s/gen #{:my.domain/name :my.domain/occupation :my.domain/id})) (gen/sample kw-gen 5)
;; To redefine our spec using this custom generator, use [`with-gen`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/with-gen) which takes a spec and a replacement generator:
(s/def ::kws (s/with-gen (s/and keyword? #(= (namespace %) "my.domain")) #(s/gen #{:my.domain/name :my.domain/occupation :my.domain/id}))) (s/valid? ::kws :my.domain/name)
(gen/sample (s/gen ::kws))
;; Note that `with-gen` (and other places that take a custom generator) take a no-arg function that returns the generator, allowing it to be lazily realized.
;; One downside to this approach is we are missing what property testing is really good at: automatically generating data across a wide search space to find unexpected problems.
;; The clojure.spec.gen.alpha namespace has a number of functions for generator "primitives" as well as "combinators" for combining them into more complicated generators.
;; Nearly all of the functions in the clojure.spec.gen.alpha namespace are merely wrappers that dynamically load functions of the same name in test.check. You should refer to the documentation for [test.check](https://clojure.github.io/test.check/) for more details on how all of the clojure.spec.gen.alpha generator functions work.
;; In this case we want our keyword to have open names but fixed namespaces. There are many ways to accomplish this but one of the simplest is to use [`fmap`](https://clojure.github.io/spec.alpha/clojure.spec.gen.alpha-api.html#clojure.spec.gen.alpha/fmap) to build up a keyword based on generated strings:
(def kw-gen-2 (gen/fmap #(keyword "my.domain" %) (gen/string-alphanumeric))) (gen/sample kw-gen-2 5)
;; `gen/fmap` takes a function to apply and a generator. The function will be applied to each sample produced by the generator allowing us to build one generator on another.
;; However, we can spot a problem in the example above - generators are often designed to return "simpler" values first and any string-oriented generator will often return an empty string which is not a valid keyword. We can make a slight adjustment to omit that particular value using [`such-that`](https://clojure.github.io/spec.alpha/clojure.spec.gen.alpha-api.html#clojure.spec.gen.alpha/such-that) which lets us specify a filtering condition:
(def kw-gen-3 (gen/fmap #(keyword "my.domain" %)
(gen/such-that #(not= % "")
(gen/string-alphanumeric)))) (gen/sample kw-gen-3 5)
;; Returning to our "hello" example, we now have the tools to make that generator:
(s/def ::hello
(s/with-gen #(clojure.string/includes? % "hello")
#(gen/fmap (fn [[s1 s2]] (str s1 "hello" s2))
(gen/tuple (gen/string-alphanumeric) (gen/string-alphanumeric)))))
(gen/sample (s/gen ::hello))
;; Here we generate a tuple of a random prefix and random suffix strings, then insert "hello" between them.
;; ### Range Specs and Generators
;; There are several cases where it’s useful to spec (and generate) values in a range and spec provides helpers for these cases.
;; For example, in the case of a range of integer values (for example, a bowling roll), use [`int-in`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/int-in) to spec a range (end is exclusive):
(s/def ::roll (s/int-in 0 11))
(gen/sample (s/gen ::roll))
;; spec also includes [`inst-in`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/inst-in) for a range of instants:
(s/def ::the-aughts (s/inst-in #inst "2000" #inst "2010"))
(drop 50 (gen/sample (s/gen ::the-aughts) 55))
;; => (#inst"2005-03-03T08:40:05.393-00:00"
;; #inst"2008-06-13T01:56:02.424-00:00"
;; #inst"2000-01-01T00:00:00.610-00:00"
;; #inst"2006-09-13T09:44:40.245-00:00"
;; #inst"2000-01-02T10:18:42.219-00:00")
;; Due to the generator implementation, it takes a few samples to get "interesting" so I skipped ahead a bit.
;; Finally, [`double-in`](https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html#clojure.spec.alpha/double-in) has support for double ranges and special options for checking special double values like `NaN` (not a number), `Infinity`, and `-Infinity`.
(s/def ::dubs (s/double-in :min -100.0 :max 100.0 :NaN? false :infinite? false))
(s/valid? ::dubs 2.9)
(s/valid? ::dubs Double/POSITIVE_INFINITY)
;; => false
(gen/sample (s/gen ::dubs))
;; To learn more about generators, read the test.check [tutorial](https://clojure.github.io/test.check/intro.html) or [examples](https://clojure.github.io/test.check/generator-examples.html). Do keep in mind that while clojure.spec.gen.alpha is a large subset of clojure.test.check.generators, not everything is included.
;; ## Instrumentation and Testing
;; spec provides a set of development and testing functionality in the `clojure.spec.test.alpha` namespace, which we can include with:
(ns porkostomus.log
(:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[clojure.spec.test.alpha :as stest]))
;; ### Instrumentation
;; Instrumentation validates that the `:args` spec is being invoked on instrumented functions and thus provides validation for external uses of a function. Let’s turn on instrumentation for our previously spec’ed `ranged-rand` function:
(stest/instrument `ranged-rand)
;; Instrument takes a fully-qualified symbol so we use \`\`\` here to resolve it in the context of the current namespace. If the function is invoked with args that do not conform with the `:args` spec you will see an error like this:
(ranged-rand 8 5)
;; The error fails in the second args predicate that checks `(< start end)`. Note that the `:ret` and `:fn` specs are not checked with instrumentation as validating the implementation should occur at testing time.
;; Instrumentation can be turned off using the complementary function `unstrument`. Instrumentation is likely to be useful at both development time and during testing to discover errors in calling code. It is not recommended to use instrumentation in production due to the overhead involved with checking args specs.
;; ### Testing
;; We mentioned earlier that `clojure.spec.test.alpha` provides tools for automatically testing functions. When functions have specs, we can use [`check`](https://clojure.github.io/spec.alpha/clojure.spec.test.alpha-api.html#clojure.spec.test.alpha/check), to automatically generate tests that check the function using the specs.
;; `check` will generate arguments based on the `:args` spec for a function, invoke the function, and check that the `:ret` and `:fn` specs were satisfied.
(stest/check `ranged-rand)
;; A keen observer will notice that `ranged-rand` contains a subtle bug. If the difference between start and end is very large (larger than is representable by `Long/MAX_VALUE`), then `ranged-rand` will produce an IntegerOverflowException. If you run `check` several times you will eventually cause this case to occur.
;; `check` also takes a number of options that can be passed to test.check to influence the test run, as well as the option to override generators for parts of the spec, by either name or path.
;; Imagine instead that we made an error in the ranged-rand code and swapped start and end:
(defn ranged-rand ;; BROKEN!
"Returns random int in range start <= rand < end"
[start end]
(+ start (long (rand (- start end)))))
;; This broken function will still create random integers, just not in the expected range. Our `:fn` spec will detect the problem when checking the var:
(stest/abbrev-result (first (stest/check `ranged-rand)))
;; => ({:spec (fspec
;; :args (and (cat :start int? :end int?) (fn* [p1__3468#] (< (:start p1__3468#) (:end p1__3468#)))) ;; :ret int?
;; :fn (and
;; (fn* [p1__3469#] (>= (:ret p1__3469#) (-> p1__3469# :args :start)))
;; (fn* [p1__3470#] (< (:ret p1__3470#) (-> p1__3470# :args :end))))),
;; :sym spec.examples.guide/ranged-rand,
;; :result {:clojure.spec.alpha/problems [{:path [:fn],
;; :pred (>= (:ret %) (-> % :args :start)),
;; :val {:args {:start -3, :end 0}, :ret -5},
;; :via [],
;; :in []}],
;; :clojure.spec.test.alpha/args (-3 0),
;; :clojure.spec.test.alpha/val {:args {:start -3, :end 0}, :ret -5},
;; :clojure.spec.alpha/failure :test-failed}}
;; `check` has reported an error in the `:fn` spec. We can see the arguments passed were -3 and 0 and the return value was -5, which is out of the expected range.
;; To test all of the spec’ed functions in a namespace (or multiple namespaces), use [`enumerate-namespace`](https://clojure.github.io/spec.alpha/clojure.spec.test.alpha-api.html#clojure.spec.test.alpha/enumerate-namespace) to generate the set of symbols naming vars in the namespace:
(-> (stest/enumerate-namespace 'user) stest/check)
;; And you can check all of the spec’ed functions by calling `stest/check` without any arguments.
;; ### Combining `check` and `instrument`
;; While both `instrument` (for enabling `:args` checking) and `check` (for generating tests of a function) are useful tools, they can be combined to provide even deeper levels of test coverage.
;; `instrument` takes a number of options for changing the behavior of instrumented functions, including support for swapping in alternate (narrower) specs, stubbing functions (by using the `:ret` spec to generate results), or replacing functions with an alternate implementation.
;; Consider the case where we have a low-level function that invokes a remote service and a higher-level function that calls it.
;; code under test
(defn invoke-service [service request]
;; invokes remote service
)
(defn run-query [service query]
(let [{::keys [result error]} (invoke-service service {::query query})] (or result error)))
;; We can spec these functions using the following specs:
(s/def ::query string?)
(s/def ::request (s/keys :req [::query]))
(s/def ::result (s/coll-of string? :gen-max 3))
(s/def ::error int?)
(s/def ::response (s/or :ok (s/keys :req [::result])
:err (s/keys :req [::error])))
(s/fdef invoke-service
:args (s/cat :service any? :request ::request)
:ret ::response)
(s/fdef run-query
:args (s/cat :service any? :query string?)
:ret (s/or :ok ::result :err ::error))
;; And then we want to test the behavior of `run-query` while stubbing out `invoke-service` with `instrument` so that the remote service is not invoked:
(stest/instrument `invoke-service {:stub #{`invoke-service}})
(invoke-service nil {::query "test"})
(stest/summarize-results (stest/check `run-query))
;; The first call here instruments and stubs `invoke-service`. The second and third calls demonstrate that calls to `invoke-service` now return generated results (rather than hitting a service). Finally, we can use `check` on the higher level function to test that it behaves properly based on the generated stub results returned from `invoke-service`.
;; ## Wrapping Up
;; In this guide we have covered most of the features for designing and using specs and generators. We expect to add some more advanced generator techniques and help on testing in a future update.
;; *Original author: Alex Miller*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment