(defproject robochef "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.9.0-alpha5"]])
(ns robochef.core
(:require [clojure.spec :as s]))
(s/def ::ingredients (s/* (s/cat :amount number?
:unit keyword?
:name string?)))
(s/describe ::ingredients)
;;=> (* (cat :amount number? :unit keyword? :name string?))
(s/valid? ::ingredients [5 :g "tea"])
;;=> true
(s/conform ::ingredients [5 :g "tea"])
;;=> [{:amount 5, :unit :g, :name "tea"}]
(s/explain-str ::ingredients ["10" :g "tea"])
;;=> "In: [0] val: \"10\" fails spec: :robochef.core/ingredients at:
;; [:amount] predicate: number?\n"
(s/exercise ::ingredients 2)
;;=> ([() []]
;; [(0 :Hi "0") [{:amount 0, :unit :Hi, :name "0"}]])
(ns robochef.core
(:require [clojure.spec :as s]))
(= ::foo :robochef.core/foo)
(= ::s/foo :clojure.spec/foo)
;; UPCOMING FEATURES
;; CLJ-1910 Namespaced maps
{:person/first "Han" :person/last "Solo" :person/ship
{:ship/name "Millenium Falcon" :ship/model "YT-1300f light freighter"}}
;;-->
#:person{:first "Han" :last "Solo" :ship
#:ship{:name "Millenium Falcon" :model "YT-1300f light freighter"}}
;; CLJ-1919 Destructuring support for namespaced keys and syms
(let [{:keys [person/first person/last person/email]} m]
(format "%s %s - %s" first last email))
Five “Regex” operators: three quantifiers (*, +, ?) + cat + alt
(s/conform (s/* keyword?) []) ;;=> []
(s/conform (s/* keyword?) [:a]) ;;=> [:a]
(s/conform (s/* keyword?) [:a :b]) ;;=> [:a :b]
(s/conform (s/+ keyword?) []) ;;=> :clojure.spec/invalid
(s/conform (s/+ keyword?) [:a]) ;;=> [:a]
(s/conform (s/+ keyword?) [:a :b]) ;;=> [:a :b]
(s/conform (s/? keyword?) []) ;;=> nil
(s/conform (s/? keyword?) [:a]) ;;=> :a
(s/conform (s/? keyword?) [:a :b]) ;;=> :clojure.spec/invalid
(s/conform (s/cat :num number?, :key keyword?) [5 :b]) ;;=> {:num 5, :key :b}
Works well with destructuring!
(s/conform (s/alt :num number?,:key keyword?) [5]) ;;=> [:num 5]
(s/conform (s/alt :num number?,:key keyword?) [:b]) ;;=> [:key :b]
Works well with core.match
!
Clojure spec also has s/or
which is similar to s/alt
(s/conform (s/or :num number?, :str string?) 5) ;;=> [:num 5]
(s/conform (s/alt :num number?, :str string?) [5]) ;;=> [:num 5]
In fact you might wonder why you need both
(s/conform (s/cat :x (s/or :num number?, :str string?)) [5]) ;;=> {:x [:num 5]}
(s/conform (s/cat :x (s/alt :num number?, :str string?)) [5]) ;;=> {:x [:num 5]}
But alt
partakes in the current regex, or
starts a new spec.
(s/def ::cat-alt (s/cat :1 (s/alt :nums (s/+ number?)
:strs (s/+ string?))
:2 (s/alt :keys (s/+ keyword?)
:syms (s/+ symbol?))))
(s/conform ::cat-alt [1 2 3 :a :b :c]) ;;=> {:1 [:nums [1 2 3]], :2 [:keys [:a :b :c]]}
(s/conform ::cat-alt ["a" "b" 'x 'y]) ;;=> {:1 [:strs ["a" "b"]], :2 [:syms [x y]]}
(s/conform ::cat-alt [1 2 :a 'b]) ;;=> :clojure.spec/invalid
(s/def ::cat-or (s/cat :1 (s/or :nums (s/+ number?)
:strs (s/+ string?))
:2 (s/or :keys (s/+ keyword?)
:syms (s/+ symbol?))))
(s/conform ::cat-or [[1 2 3] [:a :b :c]]) ;;=> {:1 [:nums [1 2 3]], :2 [:keys [:a :b :c]]}
(s/conform ::cat-or [["a" "b"] ['x 'y]]) ;;=> {:1 [:strs ["a" "b"]], :2 [:syms [x y]]}
Suppose we want a spec for the arguments or case
(comment (case x
:foo 7
:bar 42
"else"))
(s/def ::case-args (s/cat :form :clojure.spec/any
:clauses (s/* (s/cat :test :clojure.spec/any
:result :clojure.spec/any))
:else (s/? :clojure.spec/any)))
(s/conform ::case-args '(x :foo 7 :bar 42 "else"))
;;=> {:form x, :clauses [{:test :foo, :result 7} {:test :bar, :result 42}], :else "else"}
Notice how great destructuring works on this conformed data
(let [conformed (s/conform ::case-args '(x :foo 7 :bar 42 "else"))
{:keys [form clauses else]} conformed]
(doseq [{:keys [test result]} clauses]
,,,))
Putting it all together
(s/def ::hiccup (s/or :string string?
:element (s/cat :tag keyword?
:attrs (s/? map?)
:content (s/* ::hiccup))))
(s/conform ::hiccup "foo")
(s/conform ::hiccup [:div])
(s/conform ::hiccup [:div {:class "klz"}])
(s/conform ::hiccup [:div {:class "klz"} "hello"])
(s/conform ::hiccup [:div {:class "klz"} [:p "hello"] [:img {:src "/parrot.gif"}]])
(s/conform ::hiccup [:div [:p "hello"] [:img {:src "/parrot.gif"}]])
map?
::ingredient
(s/* keyword?)
(s/coll-of number? [])
(s/map-of <key-spec> <value-spec>)
(s/valid? (s/map-of keyword? number) {:x 3})
(s/coll-of <item-spec> <seed-collection>)
(s/valid? (s/coll-of string? []) ["foo"])
Most powerful: keys
Keyword used both for lookup in the map, and to find a spec in the registry
(s/def ::recipe (s/keys :req [::ingredients]
:opt [::steps]))
(s/def ::ingredients (s/* ::ingredient))
(s/def ::ingredient (s/cat :amount number?
:unit keyword?
:name string?))
(s/def ::steps (s/* string?))
(def recipe
{::ingredients [1 :kg "aubergine"
20 :ml "soy sauce"]
::steps ["fry the aubergines"
"add soy sauce"]})
(s/valid? ::recipe recipe) ;;=> true
Naturally extensible
(s/def ::recipe (s/keys :req [::ingredients]
:opt [::steps]))
(def recipe
{::ingredients [1 :kg "aubergine"
20 :ml "soy sauce"]
::steps ["fry the aubergines"
"add soy sauce"]
::feeds "4"})
(s/valid? ::recipe recipe) ;;=> true
(s/def ::feeds string?)
(s/valid? ::recipe recipe) ;;=> false
fdef
lets you set specs on the arguments, return value, and the relationship
between them.
(defn cook! [recipe]
(let [{i ::ingredients} (s/conform ::recipe recipe)]
(reduce (fn [acc {:keys [amount]}]
(+ acc amount))
0
i)))
(s/fdef cook!
:args (s/cat :recipe ::recipe)
:ret number?
:fn #(> (:ret %) (count (-> % :args :recipe ::ingredients))))
(s/instrument #'cook!)
(s/unstrument #'cook!)
(cook! recipe)
(cook! {})
This also works for macros!
and
let’s you chain multiple specs. It’s like ->
or ..
, each spec gets the
conformed value of the previous spec
(s/def ::contrived-and (s/and vector?
#(> (count %) 3)
(s/cat :sym keyword?
:rest (s/* ::s/any))
#(#{:x :y :z} (:sym %))))
(s/conform ::contrived-and [:x 1 2 3]) ;;=> {:sym :x, :rest [1 2 3]}
(s/conform ::contrived-and [:a 1 2 3]) ;;=> :clojure.spec/invalid
(require '[clojure.test.check :as tc])
(require '[clojure.test.check.generators :as gen])
(require '[clojure.test.check.properties :as prop])
(def positive-recipe-prop
(prop/for-all [r (s/gen ::recipe)]
(>= (cook! r) 0)))
(tc/quick-check 100 positive-recipe-prop)
;;=> {:result false, :seed 1465408304404, :failing-size 2, :num-tests 3, :fail [{:robochef.core/ingredients (-2.0 :+.j/l*4 "")}], :shrunk {:total-nodes-visited 26, :depth 8, :result false, :smallest [{:robochef.core/ingredients (-1.0 :A "")}]}}
(cook! {:robochef.core/ingredients [-1.0 :A ""]})