Skip to content

Instantly share code, notes, and snippets.

@udkl
Last active April 16, 2022 23:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save udkl/9e26311aaa4d02989950ba931c9a3228 to your computer and use it in GitHub Desktop.
Save udkl/9e26311aaa4d02989950ba931c9a3228 to your computer and use it in GitHub Desktop.
;; https://www.pixelated-noise.com/blog/2020/09/10/what-spec-is/
;; API docs : https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
;; https://practical.li/clojure/clojure-spec/
;; Guide : https://clojure.org/guides/spec
;; https://corfield.org/blog/2019/09/13/using-spec/
;; https://www.cognitect.com/blog/2017/6/19/improving-on-types-specing-a-java-library
;; Example code
;; https://github.com/practicalli/leveraging-spec
;; braidchat : core/client/store.cljs, base/state.cljc (validation interceptors)
;; , bots/schema.cljs, core/common/util.cljc, quests/client/core.cljs
;;;; spec libraries
; Expound : Expound formats clojure.spec error messages in a way that is optimized for humans to read.
; https://github.com/bhb/expound
; - [Inspectable](https://github.com/jpmonettas/inspectable) - Tools to explore specs and spec failures at the REPL
; - [Pretty-Spec](https://github.com/jpmonettas/pretty-spec) - Pretty printer for specs
; - [Phrase](https://github.com/alexanderkiel/phrase) - Use specs to create error messages for users
; - [Pinpointer](https://github.com/athos/Pinpointer) - spec error reporter based on a precise error analysis
; At a very fundamental level spec is a declarative language that describes data,
; their type, their shape. Spec follows the general philosophy of Clojure in that
; all of its functionality is available at runtime, you can use it, introspect it,
; generate it – there is no extra step before execution when the compiler checks
; your whole codebase for errors.
(require '[clojure.spec.alpha :as s])
(s/def ::username string?)
(println
(s/valid? ::username "foo")) ;; ==> true
; It's just predicates
(s/valid? #(> % 5) 10) ;; ==> “true
;;;; For maps --------------------->> MAPS
(s/def ::user
(s/keys
:req [::username ::password]
:opt [::comment ::last-login]))
; Spec also encourages the use of qualified keywords: Until recently in
; Clojure people would use keywords with a single colon but the two colons
; (::) mean that keywords belong to this namespace, in this case my-project.users.
; This is another deliberate choice, which is about creating strong names
; (or "fully-qualified"), that belong to a particular namespace, so that we can
; mix namespaces within the same map. This means that we can have a map that comes
; from outside our system and has its own namespace, and then we add more keys to this
; map that belong to our own company's namespace without having to worry about
; name clashes. This also helps with data provenance, because you know that the
; :subsystem-a/id field is not simply an ID – it's an ID that was assigned by subsystem-a.
;; Maps are open, so this, with ::age keyword added is true :
(println
(s/valid?
::user
{::username "rich"
::password "zegure"
::age 26}))
; This accumulation has also been described by the term "accretion" and has been discussed
; in the excellent Spec-ulation Keynote talk by Rich Hickey.
; https://www.youtube.com/watch?v=oyLBGkS5ICk
; On the other hand, a lot of people who use spec to validate things coming from
; outside their system need to be more strict with maps, and they have complained
; about the openness of maps. We'll talk about proposed solutions to this issue later.
;;; For collection (s/coll-of) ------------------>> Collections
(s/def ::username string?)
(s/def ::usernames (s/coll-of ::username))
(s/def :ex/vnum3 (s/coll-of number? :kind vector? :count 3 :distinct true :into #{}))
(s/conform :ex/vnum3 [1 2 3])
;;=> #{1 2 3}
(s/explain :ex/vnum3 #{1 2 3}) ;; not a vector
;; #{1 3 2} - failed: vector? spec: :ex/vnum3
; 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
; or for maps 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 :geom/point (s/tuple double? double? double?))
(s/conform :geom/point [1.5 2.5 -0.5])
=> [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
;;;; SEQUENCE SPECS - REGULAR EXPRESSIONS FOR DATA - mindblowing
;;;;;;;; s/cat s/conform
; s/cat allows to both validate the shape of the value passed, but it also
; enables the "conform" operation, which is somehow similar to parsing or destructuring.
; If we pass a vector of two elements – a number and a keyword – we get back a map
; with the defined names:
(s/def ::ingredient (s/cat
:quantity number?
:unit keyword?))
(prn (s/conform ::ingredient [2 :teaspoon])) ;; {:quantity 2, :unit :teaspoon}
; s/cat docs : Returns a regex op that matches (all) values in sequence, returning a map
; containing the keys of each pred and the corresponding value.
; s/conform : Given a spec and a value, returns :clojure.spec.alpha/invalid
; if value does not match spec, else the (possibly destructured) value.
; This regex combination was primarily, I think, used for validating macros
; but opens up possibilities for DSLs. This kind of code to handle optional values in
; the middle of a sequence is tricky to write in a functional way, so conform helps a lot.
;;;;;;; the spec protocol
(defprotocol Spec
(conform* [spec x])
(unform* [spec y])
(explain* [spec path via in x])
(gen* [spec overrides path rmap])
(with-gen* [spec gfn])
(describe* [spec]))
;;;;; Generators
(ns my-project.users
(:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[net.cgrand.packed-printer :as ppp]))
(s/def ::username string?)
(s/def ::password string?)
(s/def ::last-login number?)
(s/def ::comment string?)
(s/def ::user
(s/keys
:req [::username ::password]
:opt [::comment ::last-login]))
(ppp/pprint
(gen/sample (s/gen ::user) 5))
; Good talks about generators : https://www.youtube.com/watch?v=F4VZPxLZUdA
;;;;;;;;;;;
;;;;;;;; SPECS for functions
; In order to test a function with spec, you have to make three different
; specs for the three different aspects of the function.
; The first one is the :args spec which is an s/cat, and describes the arguments
; of the function. That can include specs that describe the relationship between arguments.
; You then make a spec that validates the result value of the function,
; called :ret spec. And finally you have :fn spec which is about the relationship
; between the arguments and the result of the function, if such a relationship exists.
; But the real benefit of adding specs to functions is property testing.
(require '[clojure.spec.alpha :as s]
'[clojure.spec.test.alpha :as stest]
'[clojure.pprint :as pp])
(defn num-sort [coll]
(sort coll))
(s/fdef num-sort
:args (s/cat :coll (s/coll-of number?))
:ret (s/coll-of number?)
:fn (s/and #(= (-> % :ret) (-> % :args :coll sort))
#(= (-> % :ret count) (-> % :args :coll count))))
(pp/pprint
(stest/check `num-sort))
;; -----------------
;; Examples
(defn configure [input]
(let [parsed (s/conform :ex/config input)]
(if (s/invalid? parsed)
(throw (ex-info "Invalid input" (s/explain-data :ex/config input)))
(for [{prop :prop [_ val] :val} parsed]
(set-config (subs prop 1) val)))))
;;
(defn person-name
[person]
(let [p (s/assert :acct/person person)]
(str (:acct/first-name p) " " (:acct/last-name p))))
(s/check-asserts true)
(person-name 100)
;;
(defn person-name
[person]
{:pre [(s/valid? :acct/person person)]
:post [(s/valid? string? %)]}
(str (:acct/first-name person) " " (:acct/last-name person)))
(person-name 42)
;; Execution error (AssertionError) at user/person-name (REPL:1).
;; Assert failed: (s/valid? :acct/person person)
; Next ---> https://www.cognitect.com/blog/2017/6/19/improving-on-types-specing-a-java-library
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment