Focus on spec: Combining specs with s/or

Clojure's new spec library provides the means to specify the structure of data and functions that take and return data. In this series, we'll take one Clojure spec feature at a time and examine it in more detail than you can find in the spec guide.

In our last post, we looked at s/and, a way to combine multiple specs into a compound spec. It should come as no surprise that spec also provides s/or to represent a spec made of two or more alternatives.

For example, in this ::ident spec, we have an s/or with two choices - a name represented by the string? predicate or an id represented by the int? predicate:

(s/def ::ident (s/or :name string? :id int?))
(s/valid? ::ident "abc") ;; true
(s/valid? ::ident 100)   ;; true
(s/valid? ::ident :foo)  ;; false

The first difference we see from s/and is that s/or tags the alternatives using keywords (here :name and :id). In spec we call these paths. Any time there is a spec with either alternatives or components, the parts will be named with a path name in the form of a keyword. Thus conform tells you not just that a value conformed but also how it conformed.

The conformed value will tag the result to indicate which path was taken or which component is being returned:

(s/conform ::ident "abc")
;;=> [:name "abc"]
(s/conform ::ident 100)
;;=> [:id 100]

The conformed value of s/or is a map entry, so you can either treat it like a 2-element vector with indexes 0 and 1 or invoke map entry functions like key or val on it. The key is the tag (the path) and the value is the conformed value of that path's spec.

(let [conformed (s/conform ::ident "abc")]
  (prn (key conformed))    ;; :name
  (prn (val conformed))    ;; "abc"
  (prn (nth conformed 1))) ;; "abc"

s/or will also create a generator that randomly picks one branch of the or, then invokes the generator for that branch. Let's use s/exercise to test the generator and the conformed version of the generator:

user=> (pprint (s/exercise ::ident))
([0          [:id 0]]
 [0          [:id 0]]
 [""         [:name ""]]
 [2          [:id 2]]
 ["n"        [:name "n"]]
 ["bvpq"     [:name "bvpq"]]
 [0          [:id 0]]
 [0          [:id 0]]
 ["7Hakv8Hr" [:name "7Hakv8Hr"]]
 [-1         [:id -1]])

To learn more about spec, check out the spec guide or stay tuned for more in this blog series!