-
-
Save mguinada/9c83cb3dea7049195719e7914e995d14 to your computer and use it in GitHub Desktop.
Examples of Clojure's new clojure.spec library
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
(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 "[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. | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment