Skip to content

Instantly share code, notes, and snippets.

@AlexVPopov
Last active February 21, 2020 07:08
Show Gist options
  • Save AlexVPopov/9366aff0f7c01d82a10c71db5d59bcd2 to your computer and use it in GitHub Desktop.
Save AlexVPopov/9366aff0f7c01d82a10c71db5d59bcd2 to your computer and use it in GitHub Desktop.
A cheat sheet for clojure.spec

clojure.spec cheat sheet

Specs

Require

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

Register

(s/def ::even even?)

Use a registered spec from another namespace

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

(s/def ::even ::mn/even)

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"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment