Hello @miikka I am opening this issue to discuss the exception I mentioned on PR #248 . I did some further exploration around this problem and there are 2 (at least) different scenarios going on.
The Encoder/Decoder functions should handle their own exceptions?
Calls to s/unform using values that are not compliant with s/conform.
Let's follow an example.
(s/def ::my-spec
(spec
{:spec #(and (simple-keyword? %) (-> % name clojure.string/lower-case keyword (= %)))
:description "a lowercase keyword, encoded in uppercase in string-mode"
:encode/string (fn [spec value]
(println value) ;; => :wand or ;; => "wand"
(-> value name clojure.string/upper-case))}))
;; this call produces an ::s/invalid result from s/conform however, as
;; this is a leaf node the conformer let this pass through.
(encode ::my-spec :wand string-transformer)
;; => "WAND"
The :wand keyword comply with the provided spec, but the transformed value does not ("WAND").
;; this call is problematic (in some sense) because "WAND" does not
;; comply with s/conform therefore we are "stretching" s/unform a
;; little bit here. This is a gray area for me yet, what is the
;; expected output for these situations.
(s/unform ::my-spec "WAND")
;; => "WAND"
But luckly (for me) it does not produce error.
Then we can see now the two sources of problems. When we try to encode a value that produces an error inside our encoder function, we get the cryptic message:
The error comes from here: (-> {:error-expected "wand"} name clojure.string/upper-case).
Potential solutions:
Improve documentation to explicitly guide users to handle their exceptions e.g. the spec-tools.transform namespace clearly handles all the possible exceptions for each transformation that can throw some error. The guideline is "if you don't know how to transform it, return as is".
Catch errors from transform call during the conform step and ensure the previous behavior for the user. Something like this would be enough:
;;; spec-tools.core L404
(let [transformer *transformer*, encode? *encode?*
safe-transform (fn [t this x] (try (t this x) (catch Throwable _ x)))]
;; 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 (safe-transform transform this x)]
...))
The other scenario is about calling s/unform with not compliant values becomes problem when this operation is not allowed. For example, the case motivating the PR #248 returns a string and when we call s/unform with the string we get another ClassCastException.
Hello @miikka I am opening this issue to discuss the exception I mentioned on PR #248 . I did some further exploration around this problem and there are 2 (at least) different scenarios going on.
s/unform
using values that are not compliant withs/conform
.Let's follow an example.
The
:wand
keyword comply with the provided spec, but the transformed value does not ("WAND").But luckly (for me) it does not produce error.
Then we can see now the two sources of problems. When we try to encode a value that produces an error inside our encoder function, we get the cryptic message:
The error comes from here:
(-> {:error-expected "wand"} name clojure.string/upper-case)
.Potential solutions:
Improve documentation to explicitly guide users to handle their exceptions e.g. the
spec-tools.transform
namespace clearly handles all the possible exceptions for each transformation that can throw some error. The guideline is "if you don't know how to transform it, return as is".Catch errors from
transform
call during theconform
step and ensure the previous behavior for the user. Something like this would be enough:The other scenario is about calling
s/unform
with not compliant values becomes problem when this operation is not allowed. For example, the case motivating the PR #248 returns a string and when we calls/unform
with the string we get anotherClassCastException
.I think solving this one is a lot harder, because
conform*
is returning values that are not valid (https://github.com/metosin/spec-tools/blob/d05e6e3c76c3c6ff847aa3f8e66344df2705aeae/src/spec_tools/core.cljc#L430) and if we remove this, examples like the first one I brought here would start breaking.Idk, maybe wrap this to catch errors and default to doing nothing if error occur.
Would like to hear more from you about this. Thanks