|
|
@@ -0,0 +1,118 @@ |
|
|
(ns clj-spec-playground |
|
|
(:require [clojure.string :as str] |
|
|
[clojure.spec :as s] |
|
|
[clojure.test.check.generators :as gen])) |
|
|
|
|
|
;;; examples of core.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 "[email protected]" |
|
|
::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" "[email protected]"] |
|
|
["ANthony Gonsalves" "[email protected]" "1234567890"]]) |
|
|
|
|
|
(def bad-inputs [["ANthony Gonsalves" "anthony@gmail"] |
|
|
["ANthony Gonsalves" "[email protected]" "12367890"] |
|
|
["ANthony Gonsalves" "[email protected]" 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. |
|
|
|
|
|
|