Skip to content

Commit

Permalink
Merge pull request #248 from wandersoncferreira/feature/decouple-enco…
Browse files Browse the repository at this point in the history
…de-decode-specs

add feature to provide an additional spec to encode/decode functions.
  • Loading branch information
Miikka Koskinen authored Jan 12, 2021
2 parents 2d5a4f3 + 3ef9480 commit adc8662
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 21 deletions.
42 changes: 42 additions & 0 deletions docs/10_spec_transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

```
58 changes: 37 additions & 21 deletions src/spec_tools/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@
;; Transformers
;;

(def ^:dynamic ^:private *transformer* nil)
(def ^:dynamic ^:private *encode?* nil)
(def ^:dynamic ^:private *dynamic-conforming* nil)

(defrecord DynamicConforming [transformer encode? spec-transformed])

(defprotocol Coercion
(-coerce [this value transformer options]))
Expand Down Expand Up @@ -180,15 +181,15 @@
([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
"Like `clojure.core.alpha/explain-data` but supports transformers"
([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
Expand All @@ -197,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!
Expand All @@ -208,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)
Expand All @@ -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 [*dynamic-conforming* (->DynamicConforming transformer false 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 [*dynamic-conforming* (->DynamicConforming transformer true 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."
Expand Down Expand Up @@ -400,19 +412,23 @@

s/Spec
(conform* [this x]
(let [transformer *transformer*, encode? *encode?*]
(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
(let [transformed (transform this x)]
;; short-circuit on ::s/invalid
(or (and (s/invalid? transformed) transformed)
;; recur
(let [conformed (s/conform spec transformed)]
(let [conformed (if spec-transformed
(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
(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))

Expand Down
29 changes: 29 additions & 0 deletions test/cljc/spec_tools/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit adc8662

Please sign in to comment.