Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simple implementation of unwrapping dataspecs from specs created usin… #226

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/02_data_specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,38 @@ All generated specs are wrapped into Specs Records so transformations works out
; :description "Liisa is a valid boss"
; :address nil}
```

In clojure, you can also go from the other way around, providing a
`spec` created by `ds/spec` to an `ds/unspec` function which will
recover the original data definitions.

```clj
(def person-spec
(ds/spec
{:name ::person
:spec person}))

(ds/unspec person-spec)
;; {::id integer?
;; ::age #'clojure.core/pos-int?
;; :boss boolean?
;; :name string?
;; (ds/opt :description) string?
;; :languages #{keyword?}
;; :aliases [(ds/or {:maps {:alias string?}
;; :strings string?})]
;; :orders [{:id int?
;; :description string?}]
;; :address (ds/maybe
;; {:street string?
;; :zip string?})}


```

The main differences here is related to `ds/req` and full-qualified
keywords, as we recover the values associated with these vars, the
function `pos-int?` will be returned instead of `::age`. The `ds/req`
function will not be returned because `data spec` understand the lack
of the `ds/opt` always required, therefore we have an equivalent
answer.
43 changes: 43 additions & 0 deletions src/spec_tools/data_spec.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,46 @@
:else (maybe-named-spec (st/create-spec {:spec data})))))
([name data]
(spec {:name name, :spec data})))


(defmulti unwalk (fn [{:keys [type]}]
(parse/type-dispatch-value type)) :default ::default)

(defmethod unwalk ::default [{:keys [form]}]
#?(:clj (var-get (resolve form))
:cljs @(resolve (quote form))))

(defmethod unwalk :nilable [{:keys [form]}]
(maybe (unwalk (st/create-spec (second (second form))))))

(defmethod unwalk :vector [{:keys [::parse/item form]}]
(if (= (parse/type-dispatch-value (:type item)) :or)
[(unwalk (st/create-spec {:spec (second form)}))]
[(unwalk (st/create-spec item))]))

(defmethod unwalk :map [{:keys [::parse/key->spec ::parse/keys-opt]}]
(reduce-kv
(fn [acc key val]
(if (qualified-keyword? val)
(let [maybe-key (if (contains? keys-opt key) (opt key) key)
spec (s/get-spec val)
-spec (if (st/spec? spec) spec {:spec val})]
(assoc acc maybe-key (unwalk (st/create-spec -spec))))
val))
{}
key->spec))

(defmethod unwalk :set [{:keys [::parse/item]}]
#{(unwalk (st/create-spec item))})

(defmethod unwalk :or [{:keys [form ::parse/items]}]
(let [labels (take-nth 2 (rest form))]
(or (reduce
(fn [acc [k spec]]
(assoc acc k (unwalk (st/create-spec (parse/parse-spec spec)))))
{}
(map vector labels items)))))

(defn unspec [spec]
(let [-spec (if (st/spec? spec) spec (st/create-spec {:spec spec}))]
(unwalk -spec)))
79 changes: 79 additions & 0 deletions test/cljc/spec_tools/data_spec_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
(:require [clojure.test :refer [deftest testing is]]
[clojure.spec.alpha :as s]
[spec-tools.data-spec :as ds]
#?(:clj [clojure.test.check.generators :as gen])
#?(:clj [com.gfredericks.test.chuck.clojure-test :refer [checking]])
[spec-tools.core :as st]
[spec-tools.spec :as spec])
#?(:clj
(:import clojure.lang.ExceptionInfo)))

(s/def ::age (s/and spec/integer? #(> % 10)))


(deftest data-spec-tests
(testing "nested data-spec"
(let [person {::id integer?
Expand Down Expand Up @@ -302,3 +305,79 @@
(testing "nested maps have a name"
(let [spec (st/get-spec :spec-tools.data-spec-test$named3/map)]
(is (and spec (:name spec)))))))

#?(:clj
(deftest unspecing
(testing "simpler possible specs"
(is (= int? (ds/unspec (ds/spec {:name ::t1 :spec int?}))))
(is (= string? (ds/unspec (ds/spec {:name ::t1 :spec string?}))))
(is (= float? (ds/unspec (ds/spec {:name ::t1 :spec float?}))))
(is (= boolean? (ds/unspec (ds/spec {:name ::t1 :spec boolean?}))))
(is (= keyword? (ds/unspec (ds/spec {:name ::t1 :spec keyword?})))))

(testing "simple map using data-spec"
(let [ds1 {:street string?
:number int?
:value float?
:is_main? boolean?
:clj-programmer? keyword?}]
(is (= ds1 (ds/unspec (ds/spec {:name ::ds1 :spec ds1}))))

(testing "we can handle nillable keywords"
(let [ds2 (merge ds1 {:address (ds/maybe {:street string?
:number int?})
:city string?})]
(is (= ds2 (ds/unspec (ds/spec {:name ::ds2 :spec ds2}))))))

(testing "also vector field, that also should be homogeneous."
(let [ds3 (merge ds1 {:orders [{:id int?
:description string?}]})]
(is (= ds3 (ds/unspec (ds/spec {:name ::ds3 :spec ds3}))))))

(testing "support for a set"
(let [ds4 (assoc ds1 :languages #{keyword?})
ds5 (assoc ds1 :languages #{string?})
ds6 (assoc ds1 :languages #{int?})
ds7 (assoc ds1 :languages #{boolean?})]
(is (= ds4 (ds/unspec (ds/spec {:name ::ds4 :spec ds4}))))
(is (= ds5 (ds/unspec (ds/spec {:name ::ds5 :spec ds5}))))
(is (= ds6 (ds/unspec (ds/spec {:name ::ds6 :spec ds6}))))
(is (= ds7 (ds/unspec (ds/spec {:name ::ds7 :spec ds7}))))))

(testing "support for or operator"
(let [ds8 (merge ds1 {:aliases [(or {:maps {:alias string?}
:strings string?})]})
ds9 (merge ds1 {:testing [(or {:ints int?
:bol boolean?
:val float?
:key keyword?})]})]
(is (= ds8 (ds/unspec (ds/spec {:name ::ds8 :spec ds8}))))
(is (= ds9 (ds/unspec (ds/spec {:name ::ds9 :spec ds9}))))))))))

#?(:clj
(def unspec-gen (let [-kw? (gen/return keyword?)
-str? (gen/return string?)
-bol? (gen/return boolean?)
-flt? (gen/return float?)
-int? (gen/return integer?)
-compound [-kw? -str? -bol? -flt? -int?]
-vec? (gen/vector (gen/one-of -compound) 1)
-set? (gen/set (gen/one-of -compound) {:num-elements 1})
-map? (fn [inner-gen] (gen/not-empty (gen/map gen/keyword inner-gen)))
-map-opt? (fn [inner-gen] (gen/not-empty (gen/map (gen/bind gen/keyword
(fn [k]
(gen/return (ds/opt k))))
inner-gen)))]
(gen/recursive-gen (fn [inner-gen]
(gen/frequency
[[3 (-map? inner-gen)]
[3 (-map-opt? inner-gen)]
[2 (gen/fmap (fn [v] (ds/or v)) (-map? inner-gen))]
[2 (gen/fmap (fn [v] (ds/maybe v)) (-map? inner-gen))]]))
(gen/one-of (concat -compound [-vec? -set?]))))))

#?(:clj
(deftest property-spec->unspec->spec
(checking "able to perform a complete round-trip going from data-spec -> spec -> data-spec again" 200
[data-spec unspec-gen]
(is (ds/unspec (ds/spec {:name ::property-based :spec data-spec}))))))