Last active
March 30, 2019 22:35
-
-
Save ghoseb/9f81ca592a56c23a4f7564e813d23ea5 to your computer and use it in GitHub Desktop.
Examples of Clojure's new clojure.spec library
This file contains hidden or 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
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:
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, soor
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.Problem 2: only check values in a map, they should be following spec
::problem1
?Automatic generation of
(constantly true)
is not supported but conforming works.I hope this covers all things you got stuck on.