From b34ca4e5d102b0d7d9750f3cd4ec5e8c550d1c6d Mon Sep 17 00:00:00 2001 From: Wanderson Ferreira Date: Mon, 6 Apr 2020 17:50:08 -0300 Subject: [PATCH 1/3] simple implementation of unwrapping dataspecs from specs created using dataspecs --- src/spec_tools/data_spec.cljc | 35 +++++++++++++++++ test/cljc/spec_tools/data_spec_test.cljc | 48 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/spec_tools/data_spec.cljc b/src/spec_tools/data_spec.cljc index ffde6f27..f01fb271 100644 --- a/src/spec_tools/data_spec.cljc +++ b/src/spec_tools/data_spec.cljc @@ -193,3 +193,38 @@ :else (maybe-named-spec (st/create-spec {:spec data}))))) ([name data] (spec {:name name, :spec data}))) + +(defmulti unspec (fn [{:keys [type]}] (parse/type-dispatch-value type)) :default ::default) + +(defmethod unspec ::default [{:keys [form]}] + #?(:clj (var-get (resolve form)) + :cljs @(resolve (quote form)))) + +(defmethod unspec :nilable [{:keys [form] :as sp}] + (maybe (unspec (st/create-spec (second (second form)))))) + +(defmethod unspec :vector [{:keys [form]}] + [(unspec (st/create-spec {:spec (second form)}))]) + +(defmethod unspec :map [{:keys [::parse/key->spec]}] + (reduce-kv + (fn [acc key val] + (if (qualified-keyword? val) + (assoc acc key (unspec (s/get-spec val))) + val)) + {} + key->spec)) + +(defmethod unspec :set [{:keys [::parse/item]}] + #{(unspec (st/create-spec item))}) + +(defmethod unspec :or [{:keys [form]}] + (let [form-partitioned (partition 2 (rest form))] + (or (reduce + (fn [acc [key spec-val]] + (let [-spec (second spec-val)] + (if (symbol? (:spec -spec)) + (assoc acc key (unspec (st/create-spec (parse/parse-spec (:spec -spec))))) + (assoc acc key (unspec (st/create-spec -spec)))))) + {} + form-partitioned)))) diff --git a/test/cljc/spec_tools/data_spec_test.cljc b/test/cljc/spec_tools/data_spec_test.cljc index f35d6cfe..650d1a0d 100644 --- a/test/cljc/spec_tools/data_spec_test.cljc +++ b/test/cljc/spec_tools/data_spec_test.cljc @@ -302,3 +302,51 @@ (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})))))))))) From 6e8c4add2925e1d6aafd3b27f62d725904768d63 Mon Sep 17 00:00:00 2001 From: Wanderson Ferreira Date: Mon, 6 Apr 2020 22:29:56 -0300 Subject: [PATCH 2/3] property-based tests and rename of interface to unwalk --- src/spec_tools/data_spec.cljc | 39 +++++++++++++++--------- test/cljc/spec_tools/data_spec_test.cljc | 26 ++++++++++++++++ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/spec_tools/data_spec.cljc b/src/spec_tools/data_spec.cljc index f01fb271..a9e92b63 100644 --- a/src/spec_tools/data_spec.cljc +++ b/src/spec_tools/data_spec.cljc @@ -194,37 +194,48 @@ ([name data] (spec {:name name, :spec data}))) -(defmulti unspec (fn [{:keys [type]}] (parse/type-dispatch-value type)) :default ::default) -(defmethod unspec ::default [{:keys [form]}] +(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 unspec :nilable [{:keys [form] :as sp}] - (maybe (unspec (st/create-spec (second (second form)))))) +(defmethod unwalk :nilable [{:keys [form]}] + (maybe (unwalk (st/create-spec (second (second form)))))) -(defmethod unspec :vector [{:keys [form]}] - [(unspec (st/create-spec {:spec (second form)}))]) +(defmethod unwalk :vector [{:keys [::parse/item]}] + [(unwalk (st/create-spec item))]) -(defmethod unspec :map [{:keys [::parse/key->spec]}] +(defmethod unwalk :map [{:keys [::parse/key->spec]}] (reduce-kv (fn [acc key val] (if (qualified-keyword? val) - (assoc acc key (unspec (s/get-spec val))) + (let [spec (s/get-spec val) + -spec (if (st/spec? spec) spec {:spec val})] + (assoc acc key (unwalk (st/create-spec -spec)))) val)) {} key->spec)) -(defmethod unspec :set [{:keys [::parse/item]}] - #{(unspec (st/create-spec item))}) +(defmethod unwalk :set [{:keys [::parse/item]}] + #{(unwalk (st/create-spec item))}) -(defmethod unspec :or [{:keys [form]}] +(defmethod unwalk :or [{:keys [form]}] (let [form-partitioned (partition 2 (rest form))] (or (reduce (fn [acc [key spec-val]] (let [-spec (second spec-val)] - (if (symbol? (:spec -spec)) - (assoc acc key (unspec (st/create-spec (parse/parse-spec (:spec -spec))))) - (assoc acc key (unspec (st/create-spec -spec)))))) + (cond + (symbol? (:spec -spec)) (assoc acc key (unwalk (st/create-spec (parse/parse-spec (:spec -spec))))) + (= 'clojure.spec.alpha/or (first spec-val)) + (assoc acc key (unwalk (st/create-spec {:spec spec-val}))) + :else + (assoc acc key (unwalk (st/create-spec -spec)))))) {} form-partitioned)))) + +(defn unspec [spec] + (let [-spec (if (st/spec? spec) spec (st/create-spec {:spec spec}))] + (unwalk -spec))) diff --git a/test/cljc/spec_tools/data_spec_test.cljc b/test/cljc/spec_tools/data_spec_test.cljc index 650d1a0d..d0abb6a4 100644 --- a/test/cljc/spec_tools/data_spec_test.cljc +++ b/test/cljc/spec_tools/data_spec_test.cljc @@ -2,6 +2,8 @@ (: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 @@ -9,6 +11,7 @@ (s/def ::age (s/and spec/integer? #(> % 10))) + (deftest data-spec-tests (testing "nested data-spec" (let [person {::id integer? @@ -350,3 +353,26 @@ :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)))] + (gen/recursive-gen (fn [inner-gen] + (gen/frequency + [[6 (-map? 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})))))) From a7112adc2f0d8e3a468d0ee7e13ed68013b094fb Mon Sep 17 00:00:00 2001 From: Wanderson Ferreira Date: Thu, 9 Apr 2020 00:29:21 -0300 Subject: [PATCH 3/3] cleaning some hacks, implementing ds/opt in map keys --- docs/02_data_specs.md | 35 ++++++++++++++++++++++++ src/spec_tools/data_spec.cljc | 29 +++++++++----------- test/cljc/spec_tools/data_spec_test.cljc | 9 ++++-- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/docs/02_data_specs.md b/docs/02_data_specs.md index 4e5552cf..d4ad4731 100644 --- a/docs/02_data_specs.md +++ b/docs/02_data_specs.md @@ -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. diff --git a/src/spec_tools/data_spec.cljc b/src/spec_tools/data_spec.cljc index a9e92b63..7dc1f858 100644 --- a/src/spec_tools/data_spec.cljc +++ b/src/spec_tools/data_spec.cljc @@ -205,16 +205,19 @@ (defmethod unwalk :nilable [{:keys [form]}] (maybe (unwalk (st/create-spec (second (second form)))))) -(defmethod unwalk :vector [{:keys [::parse/item]}] - [(unwalk (st/create-spec item))]) +(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]}] +(defmethod unwalk :map [{:keys [::parse/key->spec ::parse/keys-opt]}] (reduce-kv (fn [acc key val] (if (qualified-keyword? val) - (let [spec (s/get-spec 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 key (unwalk (st/create-spec -spec)))) + (assoc acc maybe-key (unwalk (st/create-spec -spec)))) val)) {} key->spec)) @@ -222,19 +225,13 @@ (defmethod unwalk :set [{:keys [::parse/item]}] #{(unwalk (st/create-spec item))}) -(defmethod unwalk :or [{:keys [form]}] - (let [form-partitioned (partition 2 (rest form))] +(defmethod unwalk :or [{:keys [form ::parse/items]}] + (let [labels (take-nth 2 (rest form))] (or (reduce - (fn [acc [key spec-val]] - (let [-spec (second spec-val)] - (cond - (symbol? (:spec -spec)) (assoc acc key (unwalk (st/create-spec (parse/parse-spec (:spec -spec))))) - (= 'clojure.spec.alpha/or (first spec-val)) - (assoc acc key (unwalk (st/create-spec {:spec spec-val}))) - :else - (assoc acc key (unwalk (st/create-spec -spec)))))) + (fn [acc [k spec]] + (assoc acc k (unwalk (st/create-spec (parse/parse-spec spec))))) {} - form-partitioned)))) + (map vector labels items))))) (defn unspec [spec] (let [-spec (if (st/spec? spec) spec (st/create-spec {:spec spec}))] diff --git a/test/cljc/spec_tools/data_spec_test.cljc b/test/cljc/spec_tools/data_spec_test.cljc index d0abb6a4..462dd9a4 100644 --- a/test/cljc/spec_tools/data_spec_test.cljc +++ b/test/cljc/spec_tools/data_spec_test.cljc @@ -363,10 +363,15 @@ -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? (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 - [[6 (-map? inner-gen)] + [[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?]))))))