-
-
Save ghoseb/9f81ca592a56c23a4f7564e813d23ea5 to your computer and use it in GitHub Desktop.
(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. | |
Never mind, I updated clojurescript to the latest version and this went away 🙅♂️
This:
(s/def ::uuid #(instance? java.util.UUID %))
might be like this:
(s/def ::uuid uuid?)
in fact uuid?
is it self a spec, like any predicate.
Hi I am wondering how you would so a spec for the contents of a map where there are options:
The map must either contain :type and either :default or :value but at least one. Also I want to be able to check for the presence of map but not worry about its keys, but check its values that are in fact the map described above? (BTW I am trying to use this to validate input from a yaml file)
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.
Unrelated, but I can't seem to figure out how to spec a collection of maps. I.e I'm getting something like this from an API:
I imagined I could do something like this, but I was wrong:
First of all I'm getting a wrong number of arguments on
coll-of
which is confusing given the examples I find online.Secondly, I tried this as well and it doesn't work:
Any input would be greatly appreciated