Fork me on GitHub
#malli
<
2023-02-04
>
Ben Sless11:02:44

(defmethod accept :or [_ _ _ _] (into #{} (map accept children))) ;;??

ikitommi14:02:04

interesting, would that work with complex types like [:or [:map [:x :int]] [:map [:y :string]]]?

Ben Sless16:02:38

Set of maps?

hanDerPeder14:02:02

this is a bug, right?

(def t (mt/transformer
        {:name :hello
         :decoders {:string (constantly "HELLO")}}))

(def schema-1 [:map [:a :string] [:b :string]])
(def schema-2 [:or [:map [:a :string] [:b :string]]])
(def schema-3 [:or [:map [:a :string]]])
(def schema-4 [:map [:a :string]])

(def v {:a 1234})

(m/decode schema-1 v t) ;; {:a "HELLO"}
(m/decode schema-2 v t) ;; {:a 1234}
(m/decode schema-3 v t) ;; {:a "HELLO"}
(m/decode schema-4 v t) ;; {:a "HELLO"}

ikitommi14:02:23

It’s a feature. :or transformer validates the result after transformation, if the result is not valid, it will ignore that branch. This could be improved, e.g. 1. take the first branch which becomes valid after the transformation (current) 2. if none is valid (new) a. take the first branch OR b. take the first branch where the value changed OR c. take the branch is is “most valid” with some value distance metering … none of these would make it bullet proof, but maybe suck less.

ikitommi14:02:38

what do you think?

ikitommi14:02:55

I think the 2a or 2b would be simple and good improvements.

ikitommi14:02:34

.. and both would yield same result in your example.

hanDerPeder14:02:20

aha, I see.. hmm, tricky.. my case requires me to have multiple transformation passes (default values, some type coercions, etc..) before it's valid. of the cases you outlined 2b would get me closest, but I still think it would fail. previously I tried using a multi-schema instead of or since all objects have an identity key, but ran into the issue mentioned in the FIXME comment; dispatch value is not valid until transformed

ikitommi14:02:30

there is an example for the dispatch key thing with :multi on README: :dispatch values should be decoded before actual values:

(m/decode
  [:multi {:dispatch :type
           :decode/string #(update % :type keyword)}
   [:sized [:map [:type [:= :sized]] [:size int?]]]
   [:human [:map [:type [:= :human]] [:name string?] [:address [:map [:country keyword?]]]]]]
  {:type "human"
   :name "Tiina"
   :age "98"
   :address {:country "finland"
             :street "this is an extra key"}}
  (mt/transformer mt/strip-extra-keys-transformer mt/string-transformer))
;{:type :human
; :name "Tiina"
; :address {:country :finland}}

hanDerPeder14:02:39

Regarding or. Couldnt you merge all the cases and transform using that?

hanDerPeder14:02:00

by no means bulletproof either, but fits with the best effort mantra of transformers

ikitommi14:02:45

> Couldnt you merge all the cases and transform using that? > could you describe how this would work?

hanDerPeder14:02:48

given or schema ?schema, do (reduce mu/merge nil (m/children ?schema)) and call transform on that

hanDerPeder15:02:36

and you wouldnt validate the result

hanDerPeder15:02:57

i do this when generating datomic pull patterns for or schemas, so i end up overfetching and clean up using strip-keys

ikitommi09:02:44

mu/merge overrides if the values are of different types (like normal merge):

(->> [:or
      [:map [:x :int]]
      [:vector :int]
      :int]
     (m/children)
     (reduce mu/merge nil))
; => :int

ikitommi09:02:45

even if they are maps, I don’t think this would work:

(->> [:or
      [:map [:x :int]]
      [:map [:x :string]]
      [:map [:x [:maybe :uuid]]]]
     (m/children)
     (reduce mu/merge nil))
; => [:map [:x [:maybe :uuid]]]

ikitommi09:02:09

e.g. if someone passes {:x "1"} it fails on the last branch, being invalid.

ikitommi09:02:25

using me/union might be ok here:

(->> [:or
      [:map [:x :int]]
      [:map [:x :string]]
      [:map [:x [:maybe :uuid]]]]
     (m/children)
     (reduce mu/union nil))
; => [:map [:x [:or [:or :int :string] [:maybe :uuid]]]]

ikitommi09:02:18

would work at least in your example:

(->> [:or [:map [:a :string] [:b :string]]]
     (m/children)
     (reduce mu/union nil))
; => [:map [:a :string] [:b :string]]

ikitommi10:02:33

here, the order matters:

(map
 (m/decoder
  (->> [:or
        [:map [:x :int]]
        [:map [:x :string]]
        [:map [:x [:maybe :uuid]]]]
       (m/children)
       (reduce mu/union nil))
  (mt/string-transformer))
 [{:x 1} {:x "1"}, {:x "kikka"}, {:x (str (random-uuid))}])
;({:x 1} 
; {:x 1} 
; {:x "kikka"} 
; {:x "ed4e918e-2e4f-49d0-a0ee-d9805ffe6384"}) ;; already ok at the string branch

ikitommi10:02:10

swapping the order:

(map
 (m/decoder
  (->> [:or
        [:map [:x [:maybe :uuid]]]
        [:map [:x :string]]
        [:map [:x :int]]]
       (m/children)
       (reduce mu/union nil))
  (mt/string-transformer))
 [{:x 1} {:x "1"}, {:x "kikka"}, {:x (str (random-uuid))}])
;({:x 1}
; {:x 1}
; {:x "kikka"}
; {:x #uuid"ed4e918e-2e4f-49d0-a0ee-d9805ffe6384"})

ikitommi10:02:26

summary: 1. mu/union gets you close, but need to be mindful about the order 2. would need more testing to verify that it doesn’t give wrong answers, e.g. with nested values 3. making default transformers depend on malli.utilit would mean than in CLJS, anyone using any transformations, would need to pull a lot of extra code for this, making the js bundle size much larger by default. Should be optional 4. if you think this would solve your case, I would be ok with: a. adding an option to :or to override the default transformation logic with a custom one

hanDerPeder12:02:28

Thanks for the thorough explanation and outlining the steps going forward. I've gone through our usage of or-schemas. Knowing what I now know about the semantics, I've yet to find a single case where a multi spec isn't preferable. If I find one I'll send a PR.

👍 2