malli 2022-05-31

This might be a bit broad question, but tl;dr is How do I implement a custom transformer? I think I want to implement a completely custom transformer to take over the entire decode/encode process - that is, I want to entirely bypass what the built-ins do, and bring my own logic to do the value transformation. I have something brewing elsewhere that I want to integrate to malli which does value type transformation on its own, and for it to work I essentially need a function callback on malli side which would get the relevant malli schema node and its properties (eg. [:string {:foo :bar}]) + the input value from data to be able to do that transformation. To me this looks like a job for custom transformer which would be created with the malli.transform/transformer helper function, but I can’t make heads or tails about what kind of code I need to write to actually enable such overriding capture of the entire transformation.

Continuing on this, I’d need a bit of clarification, see snippet:

(malli.core/decode
  [:map [:a {:foo :bar} :int]]
  {:a "string"}
  (malli.transform/transformer
    {:name :hello
     :default-decoder
     {:compile (fn [schema _]
                 (println (str "node schema: " (malli.core/-form schema)))
                 (fn [x]
                   (println (str "value:" x " / schema at val: " (malli.core/-form schema)))
                   x))}}))
node schema: [:map [:a {:foo :bar} :int]]
node schema: [:malli.core/val {:foo :bar} :int]
node schema: :int
value:{:a "string"} / schema at val: [:map [:a {:foo :bar} :int]]
value:string / schema at val: [:malli.core/val {:foo :bar} :int]
value:string / schema at val: :int
=> {:a "string"}
It seems the :a tuple is walked twice, once for the tuple itself and once for the tuple’s value. I find this confusing, as the associative nature of [:a :int] would imply the properties of the key apply to the value as well. There’s a pretty easy way to get around this, schema
[:map [:a [:int {:foo :bar}]]]
does provide properties for the :int which really was my intention, and it sort of makes sense, but it’s of course a bit kludgier. My assumption here was that if, for example, the key is marked optional then its value is naturally optional as well, and as such, all other properties should transfer to values directly as well, plus the value shouldn’t be walked on its own. So, bug, feature, gotcha or something I could trick the transformer to handle through the aforementioned _options so that it doesn’t walk to the tuple’s value? 🙂 EDIT: I created an issue about this, lets continue on GH

try :default-decoder with :compile if you want to prepare the transformer to be fast

Here’s a sample, if that’s what you are looking for:

(def transformer
  (mt/transformer
   {:name :my-transformer
    :default-decoder {:compile (fn [schema _]
                                 (println "<<<" (pr-str schema))
                                 (fn [x]
                                   (println ">>>" (pr-str schema) "->" (pr-str x))
                                   x))}}))

(def decode
  (m/decoder
   [:map
    [:x [:set [:enum "S" "L"]]]
    [:y :uuid]
    [:z [:tuple :boolean [:map [:a :int]]]]]
   transformer))
;<<< [:map [:x [:set [:enum "S" "L"]]] [:y :uuid] [:z [:tuple :boolean [:map [:a :int]]]]]
;<<< [:malli.core/val [:set [:enum "S" "L"]]]
;<<< [:set [:enum "S" "L"]]
;<<< [:enum "S" "L"]
;<<< [:malli.core/val :uuid]
;<<< :uuid
;<<< [:malli.core/val [:tuple :boolean [:map [:a :int]]]]
;<<< [:tuple :boolean [:map [:a :int]]]
;<<< :boolean
;<<< [:map [:a :int]]
;<<< [:malli.core/val :int]
;<<< :int

(decode
 {:x #{"S" "L" "XL"}
  :y :invalid
  :z [true {:a 123}]})
;>>> [:map [:x [:set [:enum "S" "L"]]] [:y :uuid] [:z [:tuple :boolean [:map [:a :int]]]]] -> {:x #{"L" "S" "XL"}, :y :invalid, :z [true {:a 123}]}
;>>> [:malli.core/val [:set [:enum "S" "L"]]] -> #{"L" "S" "XL"}
;>>> [:set [:enum "S" "L"]] -> #{"L" "S" "XL"}
;>>> [:enum "S" "L"] -> "L"
;>>> [:enum "S" "L"] -> "S"
;>>> [:enum "S" "L"] -> "XL"
;>>> [:malli.core/val :uuid] -> :invalid
;>>> :uuid -> :invalid
;>>> [:malli.core/val [:tuple :boolean [:map [:a :int]]]] -> [true {:a 123}]
;>>> [:tuple :boolean [:map [:a :int]]] -> [true {:a 123}]
;>>> :boolean -> true
;>>> [:map [:a :int]] -> {:a 123}
;>>> [:malli.core/val :int] -> 123
;>>> :int -> 123

If you want to run bottom-up, use :leave:

(def transformer
  (mt/transformer
   {:name :my-transformer
    :default-decoder {:compile (fn [schema _]
                                 (println "<<<" (pr-str schema))
                                 {:leave (fn [x]
                                           (println ">>>" (pr-str schema) "->" (pr-str x))
                                           x)})}}))

(decode
 {:x #{"S" "L" "XL"}
  :y :invalid
  :z [true {:a 123}]})
;>>> [:enum "S" "L"] -> "L"
;>>> [:enum "S" "L"] -> "S"
;>>> [:enum "S" "L"] -> "XL"
;>>> [:set [:enum "S" "L"]] -> #{"L" "S" "XL"}
;>>> [:malli.core/val [:set [:enum "S" "L"]]] -> #{"L" "S" "XL"}
;>>> :uuid -> :invalid
;>>> [:malli.core/val :uuid] -> :invalid
;>>> :boolean -> true
;>>> :int -> 123
;>>> [:malli.core/val :int] -> 123
;>>> [:map [:a :int]] -> {:a 123}
;>>> [:tuple :boolean [:map [:a :int]]] -> [true {:a 123}]
;>>> [:malli.core/val [:tuple :boolean [:map [:a :int]]]] -> [true {:a 123}]
;>>> [:map [:x [:set [:enum "S" "L"]]] [:y :uuid] [:z [:tuple :boolean [:map [:a :int]]]]] -> {:x #{"L" "S" "XL"}, :y :invalid, :z [true {:a 123}]}

Alright, thank you! What’s the second argument in :compile (fn [schema _] ... ?

_options, passed in all callbacks. Escape hatch to override anything at any time 😉

*danger-zone* 😄