(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.