Skip to content

Instantly share code, notes, and snippets.

@ghoseb
Last active March 30, 2019 22:35
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ghoseb/9f81ca592a56c23a4f7564e813d23ea5 to your computer and use it in GitHub Desktop.
Save ghoseb/9f81ca592a56c23a4f7564e813d23ea5 to your computer and use it in GitHub Desktop.
Examples of Clojure's new clojure.spec library
(ns clj-spec-playground
(:require [clojure.string :as str]
[clojure.spec :as s]
[clojure.test.check.generators :as gen]))
;;; examples of clojure.spec being used like a gradual/dependently typed system.
(defn make-user
"Create a map of inputs after splitting name."
([name email]
(let [[first-name last-name] (str/split name #"\ +")]
{::first-name first-name
::last-name last-name
::email email}))
([name email phone]
(assoc (make-user name email) ::phone (Long/parseLong phone))))
(defn cleanup-user
"Fix names, generate username and id for user."
[u]
(let [{:keys [::first-name ::last-name]} u
[lf-name ll-name] (map (comp str/capitalize str/lower-case)
[first-name last-name])]
(assoc u
::first-name lf-name
::last-name ll-name
::uuid (java.util.UUID/randomUUID)
::username (str/lower-case (str "@" ll-name)))))
;;; and now for something completely different!
;;; specs!
;;; Do NOT use this regexp in production!
(def ^:private email-re #"(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}")
(defn ^:private ^:dynamic valid-email?
[e]
(re-matches email-re e))
(defn ^:private valid-phone?
[n]
;; lame. do NOT copy
(<= 1000000000 n 9999999999))
;;; map specs
(s/def ::first-name (s/and string? #(<= (count %) 20)))
(s/def ::last-name (s/and string? #(<= (count %) 30)))
(s/def ::email (s/and string? valid-email?))
(s/def ::phone (s/and number? valid-phone?))
(def user-spec (s/keys :req [::first-name ::last-name ::email]
:opt [::phone]))
;;; play with the spec rightaway...
;;; conform can be used for parsing input, eg. in macros
(s/conform user-spec {::first-name "anthony"
::last-name "gOnsalves"
::email "a.gonsalves@GMAIL.com"
::phone 9820740784})
;;; sequence specs
(s/def ::name (s/and string? #(< (count %) 45)))
(s/def ::phone-str (s/and string? #(= (count %) 10)))
(def form-spec (s/cat :name ::name
:email ::email
:phone (s/? ::phone-str)))
;;; Specify make-user
(s/fdef make-user
:args (s/cat :u form-spec)
:ret #(s/valid? user-spec %)
;; useful to map inputs to outputs. kinda dependent typing.
;; here we're asserting that the input and output emails must match
:fn #(= (-> % :args :u :email) (-> % :ret ::email)))
;;; more specs
(s/def ::uuid #(instance? java.util.UUID %))
(s/def ::username (s/and string? #(= % (str/lower-case %))))
;;; gladly reusing previous specs
;;; is there a better way to compose specs?
(def enriched-user-spec (s/keys :req [::first-name ::last-name ::email
::uuid ::username]
:opt [::phone]))
;;; Specify cleanup-user
(s/fdef cleanup-user
:args (s/cat :u user-spec)
:ret #(s/valid? enriched-user-spec %))
;;; try these inputs
(def good-inputs [["ANthony Gonsalves" "anthony@gmail.com"]
["ANthony Gonsalves" "anthony@gmail.com" "1234567890"]])
(def bad-inputs [["ANthony Gonsalves" "anthony@gmail"]
["ANthony Gonsalves" "anthony@gmail.com" "12367890"]
["ANthony Gonsalves" "anthony@gmail.com" 1234567890]])
;;; switch instrumentation on/off
;; (do (s/instrument #'make-user)
;; (s/instrument #'cleanup-user))
;; (do (s/unstrument #'make-user)
;; (s/unstrument #'cleanup-user))
;;; if you're working on the REPL, expect to reset instrumentation multiple
;;; times.
@Sandarr95
Copy link

Sandarr95 commented Apr 7, 2017

Hi, I ran into kind of an issue in understanding. What I thought was that conform is meant to be used to get some data which follows a certain pattern to be parsed/formatted to data that your application can handle or if thats not the case return :spec/invalid. I would assume then that calling conform with the same spec on consecutive results should either always fail or always succed.
Yet:

 => (s/def ::spec-of-variant (s/alt ::number int? ::text string?))
 => (s/conform ::spec-of-variant (s/conform ::spec-of-variant [42]))
 :spec/invalid

;; to my logic, should return [::number 42]

I understand what is going wrong, I just don't understand why it's made this way and if there is a alternative.
I wanted to use alt btw because I also like to try use core.match, so or seemed irrelevant there for me.

EDIT: Thought I'd try answering the question above me, although I have a little difficulty understanding exactly what behaviour you want.

Problem 1: A map can contain keys: :type, :default & :value. It must contain at least 1 of these.

(s/def ::problem1
  (s/and
    (s/keys :opt [:type :default :value])
    (comp not empty? (partial set/intersection #{:type :default :value}) keys))) ;; this could use a seperate def

Problem 2: only check values in a map, they should be following spec ::problem1?

(s/def ::problem2 (s/map-of (constantly true) ::problem1))

Automatic generation of (constantly true) is not supported but conforming works.

I hope this covers all things you got stuck on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment