Fork me on GitHub
#malli
<
2024-03-21
>
Tommi Martin08:03:07

Would someone have examples of dispatch usage for malli that i could use to try to understand all the possibilities with it? I'm trying to improve default -> or -> [map-of|map-of|string] declaration to get better errors for myself. But I'm struggling with the nested structure and the lack of a single field that actually differentiates the maps from each other.

Tommi Martin08:03:51

Over all i'm not even sure I should be using the default system to achieve the effect but i'm not sure how else to do it. My schema is a map. This map then has several fields with string values. Followed by two nested maps. Indexed by the names of those nested maps. These nested maps then have one more level of nesting that contains the data i'm reading. so it the structure looks like this

{:name "first level"
 :field1 "value"
 :field2 "value"
 :random-name {:random-name2 {:name "submap"}}
 :random-name3 {:random-name4 {:name "the other submap"}}}
At the moment i'm achieving malli validation for that with something like this:
[::m/default
    [:or 
     [:map-of {:gen/min 1 :gen/max 1} :keyword
      [:map-of {:gen/min 1 :gen/max 3} :keyword
       [:map
        [:name "submap-1"]]]]
     [:map-of {:gen/min 1 :gen/max 1} :keyword
      [:map-of {:gen/min 1 :gen/max 3} :keyword
       [:map
        [:name "submap-2"]]]]
     [:string]]]
this works for validation. But it doesn't work for data generation. When both of the maps should be present. I would like to know if there is a better way of achieving this. The example is a bit truncated and have the other different fields between the two maps to keep the examples as short as possible. But in reality the submap-1 and 2 have a different set of fields they validate slightly differently

jussi09:03:28

We use dispatch like this. We have defined, mostly for clarity and re-usability, our schema in multiple smaller entities that are used to compose the final schema.

(def leg-event
  (mu/closed-schema
   (m/schema
    [:multi {:dispatch :vehicle
             :description "A discrete part of a trip, using one vehicle to go directly from an origin to a destination (i.e. without connecting flights). Allowed values are; flight, car, ferry and train"}
     ["flight" (m/deref-all flight {:registry registry})]
     ["ferry" (m/deref-all ferry {:registry registry})]
     ["train" (m/deref-all train {:registry registry})]
     ["car" (m/deref-all car {:registry registry})]]
    {:registry registry})))
You can then dispatch the schema like
(m/validate leg-event
            {:vehicle "train"
             :stops ["Helsinki" "Oulu" "Kittilä"]
             :distance_traveled_in_km 300})))
Not sure if this is what you are looking for but this allows certain schema validation to be triggered based on value in data.

Tommi Martin10:03:42

It's close to what I'm looking for. But unlike in your example where you can dispatch :vehicle I don't similar field that is the only source of truth with a known value I could dispatch on. Basically my dispatch would need a function to determine what to dispatch with the data I have. Kind of like

(if (map? value)
 (if (source-fields-exist? value)
  (dispatch :data-source)
  (dispatch :data-service)
 (dispatch :string))) 
If I kept using the m/default I think I would have to have a logic similar to the dirty if statement above to dispatch the correct value. But I'm not sure I can do that with Dispatch. or if I even should attempt such a hack.

Tommi Martin10:03:33

I could compute a dispatchable field before i feed the current code with the data but that feels a bit wonky in the architecture as the validation I am doing is an ingest validation before transforming the data to a different format. so manipulating the ingest data before it's transformed doesn't fit cleanly in the app i've built. But it might be the only option here

jussi10:03:25

Maybe you can use a function, it says https://github.com/metosin/malli?tab=readme-ov-file#multi-schemas that any function can be used for dispatch

Any function can be used for :dispatch:

(m/validate
  [:multi {:dispatch first}
   [:sized [:tuple keyword? [:map [:size int?]]]]
   [:human [:tuple keyword? [:map [:name string?] [:address [:map [:country keyword?]]]]]]]
  [:human {:name "seppo", :address {:country :sweden}}])
; true

1
Tommi Martin11:03:41

I'll see if i can use a custom function over inbuilt ones, thank you kindly for your input.