Fork me on GitHub
#malli
<
2023-01-31
>
flowthing13:01:17

What’s the correct way to handle this?

(malli/explain [:map [:tag [:enum :foo]]] (clojure.data.xml/sexp-as-element [:foo]))
;;=> Execution error (AbstractMethodError) at clojure.data.xml.node.Element/entryAt (node.cljc:-1).
;;   Method clojure/data/xml/node/Element.entryAt(Ljava/lang/Object;)Lclojure/lang/IMapEntry; is abstract
Do I need to walk the return value of sexp-as-element, transforming clojure.data.xml.node.Elements into maps before handing them to Malli, or is there a smarter way?

flowthing13:01:15

Something like this, perhaps?

(malli/explain Schema (malli/decode Schema xml (malli.transform/transformer {:name :my-custom-thing})))

ikitommi14:01:54

:thinking_face: looks like the Element is not implementing clojure.lang.Associative, which malli programs against. If you could PR that into the data.xml, I should just work.

ikitommi14:01:41

otherwise, you need to transform it, works, but much slower, e.g.

(m/coerce 
  [:map {:type 'clojure.data.xml.node.Element} [:tag [:enum :foo]]]
  {:tag :foo}
  custom-transformer-to-transform-the-thing)

ikitommi14:01:17

or:

(m/coerce 
  [:map {:decode/xml ...} [:tag [:enum :foo]]]
  {:tag :foo}
  (mt/transformer {:name :xml}))

flowthing17:01:06

Thanks! Yeah, that’s pretty much what I ended up with. Performance isn’t critical in this case, so this’ll do just fine. 👍

flowthing08:02:21

This solution doesn’t work with nested [:map ,,,]s, unfortunately:

(def Schema
    [:map {:decode/xml (partial into {})}
     [:tag [:enum :foo]]
     [:content
      [:+ [:map {:decode/xml (partial into {})}
           [:tag [:enum :bar]]
           [:content [:+ string?]]]]]])
  ;;=> #'user/Schema

  (malli/coerce Schema
    {:tag :foo :attrs {} :content [{:tag :bar :attrs {} :content ["1"]}]}
    (transform/transformer {:name :xml}))
  ;;=> {:tag :foo, :attrs {}, :content [{:tag :bar, :attrs {}, :content ["1"]}]}

  (try
    (malli/coerce Schema
      {:tag :foo :attrs {} :content [{:tag :BAD :attrs {} :content ["1"]}]}
      (transform/transformer {:name :xml}))
    (catch clojure.lang.ExceptionInfo ex
      (-> ex ex-data :data :explain error/humanize)))
  ;;=> {:content [{:tag ["should be :bar"]}]}

  (try
    (malli/coerce Schema
      (xml/sexp-as-element [:foo [:bar "1"]])
      (transform/transformer {:name :xml}))
    (catch clojure.lang.ExceptionInfo ex
      (-> ex ex-data :data :explain error/humanize)))
  ;;=> {:tag :foo, :attrs {}, :content [{:tag :bar, :attrs {}, :content ["1"]}]}

  (try
    (malli/coerce Schema
      (xml/sexp-as-element [:foo [:BAD "1"]])
      (transform/transformer {:name :xml}))
    (catch clojure.lang.ExceptionInfo ex
      (-> ex ex-data :data :explain error/humanize)))
  ;;=> Execution error (AbstractMethodError) at clojure.data.xml.node.Element/entryAt (node.cljc:-1).
  ;;   Method clojure/data/xml/node/Element.entryAt(Ljava/lang/Object;)Lclojure/lang/IMapEntry; is abstract
Dunno whether custom-transformer-to-transform-the-thing would solve this problem, but I’m unclear on how I’d implement that, since I can’t find any documentation or examples on how to implement custom transformers like that.

ikitommi08:02:01

Something like:

(def elements-to-maps-transformer
  "transforms all Elements into maps"
  (mt/transformer
   {:decoders {:map #(cond->> % (instance? Element %) (into {}))}}))

ikitommi08:02:59

1. apply only to :map schemas 2. run a function that checks if it’s an Element, then make it a map

ikitommi09:02:05

… and you can compose, the order matters:

(mt/transformer
  elements-to-maps-transformer ;; this first 
  (mt/json-transformer)) ;; ... so these see maps, not emelents

flowthing09:02:18

Thanks, I was missing the :map bit. 👍 Still no dice, though, unfortunately: Malli only applies the transformation to the root value.

flowthing09:02:39

I think the simplest solution in this case is for me to walk the thing sexp-as-element returns and turn Elements to maps before handing the value to Malli.

ikitommi09:02:13

oh, true. into (into {} x) is not recursive. change it to (clojure.walk/postwalk #(cond->> % (instance? Element %) (into {}))) and should work

flowthing09:02:41

Yep, could do that, but I don’t think that has any benefit over just transforming the value beforehand in this case.

Joel03:07:43

Not sure what the final solution was on this thread. In my case I’m invoking m/schema and getting clojure.lang.ExceptionInfo: :malli.core/invalid-schema

shem17:01:44

{:static1 "foo"
:static2 "bar"
:var-1234 "baz"
:var-9725 "euromokko"}

shem17:01:38

if we have maps like these, where the static* keys are known and the numbers ending the var* keys are not known and may be any integer, is it possible to construe a schema that also covers the var* keys?

respatialized17:01:20

you could use :map-of and a predicate schema to constrain the names of the var-* keys, then :merge with the static keys

shem18:01:17

right...i'll experiment with that. thanks!

shem18:01:44

(def samppeli {:static1 "foo"
                 :static2 "bar"
                 :var-1234a "baz"
                 :var-9725 "euromokko"})
  (def varskeema (m/schema [:map-of #"^var-\d+$" :string]))
  (def staticskeema (m/schema [:map [:static1 :string]
                               [:static2 :string]]))
  (def skeema  (mu/merge varskeema staticskeema))

shem18:01:19

this validates ok even though it shouldn't, probably because maps are open by default. i can't seem to find the right place for {:closed true}

respatialized19:01:56

Try using mu/`update-properties` on the results of mu/merge

escherize20:01:15

or mu/closed-schema on it

shem17:01:12

i couldn't find pattern matching in the keys

escherize20:01:08

We are gonna need some more info if you are trying to get help.

dvingo21:01:43

with the experimental syntax is there a way to specify a unique return type per arity? I only see one in the tests/examples.

(mx/defn a-fun  ;; :- [:int {:min 0}] <--- instead of this
  "docstring"
  ([x :- [:int {:min 0}]] :- :int (inc x))
  ([x :- [:int {:min 0}], y :- :int] :- [:int {:min 10}] (+ x y))
  ([x :- [:int {:min 0}], y :- :int & zs :- [:* :int]] :- :int  (apply + x y zs)))
something like that, I'm not sure where the syntax would go. With :function schema annotations this is supported, so it's just a matter of where the syntax lives and how it's transformed.

escherize21:01:43

I don’t think so. I was looking at that the other day. notice that in the tests there is no such thing.

escherize21:01:19

you can of course use m/=> to get that behavior. but I don’t think mx/defn can

dvingo22:01:55

cool cool - thanks for confirming!

✌️ 2
escherize23:01:31

Been noodling on this one for a while. I think I have a solution, but would be nice to hear thoughts: I want to conditionally validate fxn annotations for input/output based on the value of an atom (a switch to turn it off or on). I am thinking of 3 modes: 1. usually : validate iff @*validate-schemas. then either of these will override *validate-schemas: 2. ^:never-validate : never validates 3. ^:always-validate : always validates spoiler: Currently I am emitting a call to instrument!, but I am thinking that it makes sense to use -strument, and use a function in mode… Then again it’s probably better to just require the user to specify their auto validation wants before we start defining functions.

escherize16:02:12

Just saw the PR for this! Looking forward to seeing some more improvements on mx/defn. 🆒