Last active
April 16, 2022 23:20
-
-
Save udkl/9e26311aaa4d02989950ba931c9a3228 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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