Last Updated: December 31, 2020
·
1.014K
· alexvpopov

clojure.spec cheatsheet

clojure.spec cheat sheet

Specs

Require

(ns my.ns
  (:require [clojure.spec.alpha :as s]))

Register

(s/def ::even even?)

(s/valid? ::even 4)

Use a registered spec from another namespace

(require '[my.namespace :as mn])

(s/valid? ::mn/even 4)

Validate

(s/conform even? 4) ; returns the value or :clojure.spec.alpha/invalid

(s/valid? even? 4) ; returns true or false

Get validation errors

(s/explain-data even? 4) ; => nil

(s/explain-data even? 5) 
; => #:clojure.spec.alpha{:problems [{:path [], :pred clojure.core/even?, :val 5, :via [], :in []}],
;                         :spec #function[clojure.core/even?],
;                         :value 5}

Compose

(s/def ::even-and-above-10 (s/and even? #(> % 10)))

(s/valid? ::even-and-above-10 12) ; => true

(s/valid? ::even-and-above-10 8) ; => false

Entity maps

(s/def ::name string?)

(s/def ::nickname string?)

(s/def ::age int?)

(s/def ::person (s/keys :req-un [::name ::age] :opt-un [::nickname]))

(s/explain-data ::person {:name "Pesho" :age 30 :nickname "10"}) ; => nil

(s/explain-data ::person {:name "Pesho" :age 30 :nickname 10})

; #:clojure.spec.alpha{:problems
;                      ({:path [:nickname],
;                        :pred clojure.core/string?,
;                        :val 10,
;                        :via [:user/person :user/nickname],
;                        :in [:nickname]}),
;                      :spec :user/person,
;                      :value {:name "Pesho", :age 30, :nickname 10}}

Generators

Require

(require '[clojure.spec.gen.alpha :as gen])

Generator

(s/gen pos-int?)

Compound generator

(s/gen (s/and pos-int? even?))

Generate a value

(gen/generate (s/gen pos-int?)) ; => 14

Custom generators

from spec

This works by clojure.spec generating values from the base spec (in this case string?) and then applying the subsequent predicates as filters. Since it only generates 100 initial values, this will probably not work for complex specs.

(def uuid-regex #"(?i)^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")

(s/def ::uuid-string (s/and string? #(some? (re-matches uuid-regex %))))

(s/valid? ::uuid-string "c278820c-9734-4ac2-99f6-23dd959ee73c") ; => true

(gen/generate (s/gen ::uuid-string))
; => 
; Error:
; Execution error (ExceptionInfo) at clojure.test.check.generators/fn (generators.cljc:435).
; Couldn't satisfy such-that predicate after 100 tries.

with-gen

Takes a spec and a function, returning a generator, and returns a spec, which uses the generator to generate values.

(s/def ::my-even (s/with-gen 
                   (s/and pos-int? even?)
                   #(s/gen #{2 100 980})))

(gen/generate (s/gen ::my-even)) ; => one of 2, 100 or 980

fmap

Takes a function and a generator and returns a generator, whose values are transformed by the function

(gen/generate (s/gen uuid?)) ; => #uuid "a06baf1e-3d77-49b4-8279-bceb5cd74ecd"

(gen/generate (gen/fmap str (s/gen uuid?))) ; => "a06baf1e-3d77-49b4-8279-bceb5cd74ecd"

Spec with custom generator

(def uuid-regex #"(?i)^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$")

(s/def ::uuid-string
  (s/with-gen (s/and string? #(some? (re-matches uuid-regex %)))
    #(gen/fmap str (s/gen ::uuid))))

(gen/generate (s/gen ::uuid-string)) ; => "a06baf1e-3d77-49b4-8279-bceb5cd74ecd"