Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Examples of Clojure's new clojure.spec library
(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.
@viebel

This comment has been minimized.

Copy link

commented Jan 1, 2017

Would you like to make this gist interactive with klipse?

@sdave2

This comment has been minimized.

Copy link

commented Jan 6, 2017

(defrecord Car [name type num-of-wheels make])

How would I generate random data for a record like the one above?

@sdave2

This comment has been minimized.

Copy link

commented Jan 6, 2017

Okay, so this is what I came up with.

`(s/def ::name string?)
(s/def ::type keyword?)
(s/def ::wheels int?)
(s/def ::make string?)

(defn car-gen
[]
(gen/bind
(s/gen (s/spec (s/keys :req [::name ::type ::wheels ::make])))
#(gen/return (map->Car %))))

(s/def ::car (s/spec (s/keys :req [::name ::type ::wheels ::make])
:gen car-gen))

(gen/generate (car-gen))

(clojure.pprint/pprint (drop 198 (s/exercise ::car 200)))`

The code doesn't look too good. What would be a good alternative?

Thanks.

@bowd

This comment has been minimized.

Copy link

commented Feb 13, 2017

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:

{
  ...
  lines: [
    { text: "Some text", attachments: null},
    { text: null, attachments: [{type: "image", payload: { url: "http://some-url" }} ] }
   ]
  ...
}

I imagined I could do something like this, but I was wrong:

(s/def ::text string?)
(s/def ::type string?)
(s/def ::payload (s/keys :opt-un [::url]))
(s/def ::attachment (s/keys :req-un [::type ::payload]))
;; bad usage of coll-of
(s/def ::attachments (s/coll-of ::attachment))

(s/def ::line (s/keys :opt-un [::text :attachments]))
;; bad usage of coll-of
(s/def ::lines (s/coll-of ::line))

;; top-level object
(s/def ::note (s/keys :req-un [::lines]))

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:

(def attachment? (partial s/valid? ::attachment))
(s/def ::attachments (s/coll-of attachment?))

Any input would be greatly appreciated

@bowd

This comment has been minimized.

Copy link

commented Feb 13, 2017

Never mind, I updated clojurescript to the latest version and this went away 🙅‍♂

@selimober

This comment has been minimized.

Copy link

commented Feb 23, 2017

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.

@mmeroberts

This comment has been minimized.

Copy link

commented Mar 1, 2017

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)

@Sandarr95

This comment has been minimized.

Copy link

commented Apr 7, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.