From eefe77e5d853d8712cbe5149b4556a1fabebeafc Mon Sep 17 00:00:00 2001 From: Wanderson Ferreira Date: Sat, 14 Nov 2020 09:04:57 -0300 Subject: [PATCH 1/2] add feature to provide an additional spec to encode/decode functions. The idea is to validate the target transformed value. The current implementation will only make transformations that does not break the provided spec. --- docs/10_spec_transformations.md | 42 ++++++++++++++++++++++++++ src/spec_tools/core.cljc | 46 +++++++++++++++++++---------- test/cljc/spec_tools/core_test.cljc | 29 ++++++++++++++++++ 3 files changed, 102 insertions(+), 15 deletions(-) diff --git a/docs/10_spec_transformations.md b/docs/10_spec_transformations.md index 5ac1561b..10d661bf 100644 --- a/docs/10_spec_transformations.md +++ b/docs/10_spec_transformations.md @@ -255,3 +255,45 @@ Or using `type-transformer` directly: st/strip-extra-keys-transformer st/strip-extra-values-transformer)) ``` + +It is also possible to add a spec to be used to validate the +transformed value. Using this feature you decouple the transformation +into two specs, the original schema before transformation and the +target schema after transformation. + +```clj +(s/def :db/hostname string?) +(s/def :db/port pos-int?) +(s/def :db/database string?) +(s/def ::jdbc-connection + (st/spec {:spec (s/keys :req-un [:db/hostname :db/port :db/database]) + :type :dbconn})) + +(defn dbconn->url + [_ {:keys [hostname port database]}] + (format "jdbc:postgres://%s:%s/%s" hostname port database)) + +(def jdbc-transformer + (st/type-transformer + {:name :jdbc + :encoders {:dbconn dbconn->url} + :default-encoder stt/any->any})) + +(st/encode + ::jdbc-connection + {:hostname "127.0.0.1" :port 5432 :database "postgres"} + jdbc-transformer) + +;; => ::s/invalid + +(s/def :db/conn-string string?) + +(st/encode + ::jdbc-connection + {:hostname "127.0.0.1" :port 5432 :database "postgres"} + jdbc-transformer + :db/conn-string) + +;; => "jdbc:postgres://127.0.0.1:5432/postgres" + +``` diff --git a/src/spec_tools/core.cljc b/src/spec_tools/core.cljc index bef8c10a..af74009d 100644 --- a/src/spec_tools/core.cljc +++ b/src/spec_tools/core.cljc @@ -73,6 +73,7 @@ (def ^:dynamic ^:private *transformer* nil) (def ^:dynamic ^:private *encode?* nil) +(def ^:dynamic ^:private *spec-transformed* nil) (defprotocol Coercion (-coerce [this value transformer options])) @@ -232,31 +233,42 @@ (defn decode "Decodes a value using a [[Transformer]] from external format to a value defined by the spec. First, calls [[coerce]] and returns the value if it's - valid - otherwise, calls [[conform]] & [[unform]]. Returns `::s/invalid` + valid - otherwise, calls [[conform]] & [[unform]]. You can also provide a + spec to validate the decoded value after transformation. Returns `::s/invalid` if the value can't be decoded to conform the spec." ([spec value] (decode spec value nil)) ([spec value transformer] + (decode spec value transformer nil)) + ([spec value transformer spec-transformed] (let [spec (into-spec spec) coerced (coerce spec value transformer)] (if (s/valid? spec coerced) coerced - (binding [*transformer* transformer, *encode?* false] + (binding [*transformer* transformer, *encode?* false *spec-transformed* spec-transformed] (let [conformed (s/conform spec value)] (if (s/invalid? conformed) conformed - (s/unform spec conformed)))))))) + (if spec-transformed + (s/unform spec-transformed conformed) + (s/unform spec conformed))))))))) (defn encode "Transforms a value (using a [[Transformer]]) from external - format into a value defined by the spec. On error, returns `::s/invalid`." - [spec value transformer] - (binding [*transformer* transformer, *encode?* true] - (let [spec (into-spec spec) - conformed (s/conform spec value)] - (if (s/invalid? conformed) - conformed - (s/unform spec conformed))))) + format into a value defined by the spec. You can also provide a + spec to validate the encoded value after transformation. + On error, returns `::s/invalid`." + ([spec value transformer] + (encode spec value transformer nil)) + ([spec value transformer spec-transformed] + (binding [*transformer* transformer, *encode?* true, *spec-transformed* spec-transformed] + (let [spec (into-spec spec) + conformed (s/conform spec value)] + (if (s/invalid? conformed) + conformed + (if spec-transformed + (s/unform spec-transformed conformed) + (s/unform spec conformed))))))) (defn select-spec "Best effort to drop recursively all extra keys out of a keys spec value." @@ -400,7 +412,7 @@ s/Spec (conform* [this x] - (let [transformer *transformer*, encode? *encode?*] + (let [transformer *transformer*, encode? *encode?*, spec-transformed *spec-transformed*] ;; if there is a transformer present (if-let [transform (if transformer ((if encode? -encoder -decoder) transformer (decompose-spec-type this) x))] ;; let's transform it @@ -408,11 +420,15 @@ ;; short-circuit on ::s/invalid (or (and (s/invalid? transformed) transformed) ;; recur - (let [conformed (s/conform spec transformed)] + (let [conformed (if spec-transformed + (binding [*spec-transformed* nil *transformer* nil] + (s/conform spec-transformed transformed)) + (s/conform spec transformed))] ;; it's ok if encode transforms leaf values into invalid values - (or (and encode? (s/invalid? conformed) (leaf? this) transformed) conformed)))) + (or (and spec-transformed conformed) + (and encode? (s/invalid? conformed) (leaf? this) transformed) + conformed)))) (s/conform spec x)))) - (unform* [_ x] (s/unform spec x)) diff --git a/test/cljc/spec_tools/core_test.cljc b/test/cljc/spec_tools/core_test.cljc index b1bf46df..b5aef126 100644 --- a/test/cljc/spec_tools/core_test.cljc +++ b/test/cljc/spec_tools/core_test.cljc @@ -858,6 +858,35 @@ :ref (s/map-of qualified-keyword? (s/or :rec-pattern ::rec-pattern))))) +(s/def :db/hostname string?) +(s/def :db/port pos-int?) +(s/def :db/database string?) +(s/def ::jdbc-connection + (st/spec {:spec (s/keys :req-un [:db/hostname :db/port :db/database]) + :type :dbconn})) + +(defn dbconn->url + [_ {:keys [hostname port database]}] + #?(:clj (clojure.core/format "jdbc:postgres://%s:%s/%s" hostname port database) + :cljs (cljs.pprint/cl-format nil "jdbc:postgres://~a:~a/~a" hostname port database))) + +(def jdbc-transformer + (st/type-transformer + {:name :jdbc + :encoders {:dbconn dbconn->url} + :default-encoder stt/any->any})) + +(s/def :db/connection-string (st/spec {:spec string? + :type :dbconn})) + +(deftest issue-241 + (testing "provide a spec to validate transformed values" + (let [valid-input {:hostname "127.0.0.1" :port 5432 :database "postgres"}] + (is (thrown? #?(:clj ClassCastException :cljs js/Error) + (st/encode ::jdbc-connection valid-input jdbc-transformer))) + (is (= (st/encode ::jdbc-connection valid-input jdbc-transformer :db/connection-string) + "jdbc:postgres://127.0.0.1:5432/postgres"))))) + (deftest issue-244 (testing "stop rewalking recursive specs." (let [correct-data [:core-test/stack From 3ef9480d4634a1f58aae0e7a4af51352c18d3582 Mon Sep 17 00:00:00 2001 From: Wanderson Ferreira Date: Sun, 10 Jan 2021 22:13:24 -0300 Subject: [PATCH 2/2] group related dynamic bindings into single dynamic record --- src/spec_tools/core.cljc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/spec_tools/core.cljc b/src/spec_tools/core.cljc index af74009d..9fcb021c 100644 --- a/src/spec_tools/core.cljc +++ b/src/spec_tools/core.cljc @@ -71,9 +71,9 @@ ;; Transformers ;; -(def ^:dynamic ^:private *transformer* nil) -(def ^:dynamic ^:private *encode?* nil) -(def ^:dynamic ^:private *spec-transformed* nil) +(def ^:dynamic ^:private *dynamic-conforming* nil) + +(defrecord DynamicConforming [transformer encode? spec-transformed]) (defprotocol Coercion (-coerce [this value transformer options])) @@ -181,7 +181,7 @@ ([spec value] (explain spec value nil)) ([spec value transformer] - (binding [*transformer* transformer, *encode?* false] + (binding [*dynamic-conforming* (->DynamicConforming transformer false nil)] (s/explain (into-spec spec) value)))) (defn explain-data @@ -189,7 +189,7 @@ ([spec value] (explain-data spec value nil)) ([spec value transformer] - (binding [*transformer* transformer, *encode?* false] + (binding [*dynamic-conforming* (->DynamicConforming transformer false nil)] (s/explain-data (into-spec spec) value)))) (defn conform @@ -198,7 +198,7 @@ ([spec value] (conform spec value nil)) ([spec value transformer] - (binding [*transformer* transformer, *encode?* false] + (binding [*dynamic-conforming* (->DynamicConforming transformer false nil)] (s/conform (into-spec spec) value)))) (defn conform! @@ -209,7 +209,7 @@ ([spec value] (conform! spec value nil)) ([spec value transformer] - (binding [*transformer* transformer, *encode?* false] + (binding [*dynamic-conforming* (->DynamicConforming transformer false nil)] (let [spec' (into-spec spec) conformed (s/conform spec' value)] (if-not (s/invalid? conformed) @@ -245,7 +245,7 @@ coerced (coerce spec value transformer)] (if (s/valid? spec coerced) coerced - (binding [*transformer* transformer, *encode?* false *spec-transformed* spec-transformed] + (binding [*dynamic-conforming* (->DynamicConforming transformer false spec-transformed)] (let [conformed (s/conform spec value)] (if (s/invalid? conformed) conformed @@ -261,7 +261,7 @@ ([spec value transformer] (encode spec value transformer nil)) ([spec value transformer spec-transformed] - (binding [*transformer* transformer, *encode?* true, *spec-transformed* spec-transformed] + (binding [*dynamic-conforming* (->DynamicConforming transformer true spec-transformed)] (let [spec (into-spec spec) conformed (s/conform spec value)] (if (s/invalid? conformed) @@ -412,7 +412,7 @@ s/Spec (conform* [this x] - (let [transformer *transformer*, encode? *encode?*, spec-transformed *spec-transformed*] + (let [{:keys [transformer encode? spec-transformed]} *dynamic-conforming*] ;; if there is a transformer present (if-let [transform (if transformer ((if encode? -encoder -decoder) transformer (decompose-spec-type this) x))] ;; let's transform it @@ -421,7 +421,7 @@ (or (and (s/invalid? transformed) transformed) ;; recur (let [conformed (if spec-transformed - (binding [*spec-transformed* nil *transformer* nil] + (binding [*dynamic-conforming* (->DynamicConforming nil encode? nil)] (s/conform spec-transformed transformed)) (s/conform spec transformed))] ;; it's ok if encode transforms leaf values into invalid values