Fork me on GitHub
#malli
<
2023-02-07
>
renewdoit05:02:11

m/entries wraps the entries into [::m/varl :int] like reified structure, how do I unwrap the entry get that :int out?

renewdoit06:02:17

Answer by my own: m/children can access it.

šŸ‘ 2
marrs17:02:29

I'm trying to create a schema for a map that can contain one of a number of keys, e.g. a login form schema that can accept :username or :email. I'm sure it must be possible to create a schema to represent this but I can't find mention of it in the docs. I want to write something like

(def schema
  [:map
   [:or
    [:username [:string]]
    [:email [:string]]
   [:password [:string]]])

Noah Bogart18:02:35

Could use an :fn schema:

(def my-schema
  [:and
   [:map
    [:x int?]
    [:y int?]]
   [:fn (fn [{:keys [x y]}] (or x y))]])

āž• 2
marrs18:02:24

Thanks, I'll think about that

escherize18:02:24

multi schema?

escherize18:02:48

nah, thatā€™s not quite right ^

escherize18:02:16

Iā€™d go with @UEENNMX0Tā€™s fn approach

ikitommi19:02:13

Yes, there is no declarative syntax for dependent keys. One could cook up that with :multi but would be quite ugly. This could be nice (and doable in the user space):

(def schema
  [:and
   [:map
    [:username {:optional true} [:string]]
    [:email {:optional true} [:string]]
    [:password [:string]]]
   [:keys/xor [:username :email]]])

ikitommi19:02:17

but, not sure if that is worth it. :fn is quite clear already.

ikitommi20:02:24

yes, tried to find that as an example here, didnā€™t, thanks!

āž• 2
marrs09:02:48

Thanks, everyone. I ended up making 2 different schemas and choosing between them at runtime depending on which field was presented in the submitted form.

marrs09:02:37

> Yes, there is no declarative syntax for dependent keys. One could cook up that with :multi but would be quite ugly. This could be nice (and doable in the user space): >

(def schema
>   [:and
>    [:map
>     [:username {:optional true} [:string]]
>     [:email {:optional true} [:string]]
>     [:password [:string]]]
>    [:keys/xor [:username :email]]])
> The only thing about this (and its equivalent :fn version) is what would happen if neither :username nor :email were submitted. Both fields are declared as optional which implies that the form should validate, but that's not correct. Each field is required if the other isn't present.

marrs17:03:48

@UEENNMX0T I came back to this problem today and I'm struggling to implement your proposed solution. Given the code:

(humanize (explain (malli/schema [:and
                                    [:map
                                     [:id int?]
                                     [:username string?]]
                                    [:fn (fn [{:keys [id username]}]
                                           (or id username))]])
                     {:id 1}))
I get {:username ["missing required key"]} as a result but my intention is that there should be no error as id has been correctly provided.

Noah Bogart17:03:40

might need {:optional true}

marrs17:03:03

bah, I realised that as soon as I posted šŸ˜›

Panel23:02:47

Hi, I hack something up to convert malli schema to EQL querry, it support recursive query. Here's the code with an example at the bottom that convert the burger schema from http://malli.io

(ns malli-eql
  (:require
   [malli.core :as m]
   [malli.util :as mu]))

(defn trim-branch-path [paths]
  (reduce (fn [acc item]
            (if (some #{item} (map #(vec (take (count item) %)) acc))
              acc
              (conj (vec (remove #(#{(butlast item)} %)
                                 acc))
                    item)))
          []
          paths))

(defn -collect [schema]
  (let [state (atom {})]
    (m/walk
     schema
     (fn [schema _ _ _]
       (let [properties (m/properties schema)]
         (doseq [[k v] (-> (m/-properties-and-options properties (m/options schema) identity) first :registry)]
           (swap! state assoc-in [:registry k] v))
         (swap! state assoc :schema schema)))
     {::m/walk-schema-refs true})
    @state))

(defn map-vec-tree-seq [form]
  (tree-seq #(or (sequential? %)
                 (associative? %))
            (fn [form]
              (cond (sequential? form) (next form)
                    (associative? form) (#(interleave (keys %) (vals %)) form)))
            form))
(defn recursive-registry [schema]
  (->> (-> schema m/schema -collect :registry)
       (map (fn [[k v]]
              [k
               (->> v
                    mu/subschemas
                    trim-branch-path
                    (map #(if (and (= (m/type (:schema %)) :ref)
                                   (= k (first (m/children (:schema %)))))
                            (assoc % :in (conj (:in %) '...))
                            %))
                    (map :in))]))
       (filter (fn [[_k v]] (some #{'...} (map-vec-tree-seq v))))
       (into {})))

(defn path->eql [path]
  (let [p' (->> path
                (remove #{:malli.core/in}))

        eql (reduce #(hash-map %2 %1) (if (= '... (last p')) (last p') [(last p')]) (reverse (drop-last p')))]
    (or (if (vector? eql)
          (first eql)
          eql) [])))

(defn deep-merge-eql [forms]
  (reduce (fn f'
            ([] nil)
            ([a b]
                  (let [s (when (and (map? b) (sequential? a))
                            (some
                             (fn [x'] (when (get x' (ffirst b))
                                        x'))
                             a))
                        r (cond (and (vector? a)
                                     (vector? b))
                                (vec (into #{} (concat a b)))

                                s (conj (vec (remove #{s} a)) (deep-merge-eql [s b]))
                                (and (vector? a)
                                     (map? b)
                                     (some #{(ffirst b)} a)) (conj (vec (remove #{(ffirst b)} a)) b)
                                (and (vector? a)
                                     (map? b)) (conj a b)
                                (and (map? a)
                                     (map? b)
                                     (= (ffirst a) (ffirst b))) (merge-with f' a b)
                                (and (map? a)
                                     (map? b)) [a b]
                                (and (vector? b)
                                     (map? a)
                                     (some #{(ffirst a)} b)) (conj (vec (remove #{(ffirst a)} b)) a)
                                (and (map? a)
                                     (vector? b)) (conj b a)
                                (vector? a) (vec (into #{} (conj a b)))
                                (vector? b) (vec (into #{} (conj b a)))
                                :else
                                (vector a b))]
                    r)))
          forms))

(defn schema->paths [schema registry]
  (let [paths (atom [])
        collect-paths-walker! (fn walker'
          ([s] (walker' s []))
          ([s parents]
           (m/walk s (fn [schema path _children _options]
                       (let [is-ref (and (m/-ref-schema? schema)
                                         (m/-ref schema))
                             ref-name (when is-ref (m/-ref schema))
                             in (mu/path->in (m/schema s) path)]
                         (cond (and is-ref
                                    (some #{ref-name} (keys registry)))
                               (mapv #(swap! paths
                                             conj
                                             (vec (concat parents in %)))
                                     (get registry ref-name))

                               (and is-ref ref-name)
                               (walker' (m/deref schema) (vec (concat parents (mu/path->in (m/schema s) path))))
                               :else (swap! paths conj (vec (concat parents in)))))))))]
    (collect-paths-walker! schema)
    @paths))

(defn schema->eql [schema]
  (->> (schema->paths schema (recursive-registry schema))
       trim-branch-path
       (map path->eql)
       deep-merge-eql))

(schema->eql [:schema
              {:registry {"Country" [:map
                                     {:closed true}
                                     [:name [:enum :FI :PO]]
                                     [:neighbors
                                      {:optional true}
                                      [:vector [:ref "Country"]]]],
                          "Burger" [:map
                                    [:name string?]
                                    [:description {:optional true} string?]
                                    [:origin [:maybe "Country"]]
                                    [:price pos-int?]],
                          "OrderLine" [:map
                                       {:closed true}
                                       [:burger "Burger"]
                                       [:amount int?]],
                          "Order" [:map
                                   {:closed true}
                                   [:lines [:vector "OrderLine"]]
                                   [:delivery
                                    [:map
                                     {:closed true}
                                     [:delivered boolean?]
                                     [:address
                                      [:map
                                       [:street string?]
                                       [:zip int?]
                                       [:country "Country"]]]]]]}}
              "Order"])
;; => [{:lines
;;      [:amount
;;       {:burger [:description :name {:origin [:name {:neighbors ...}]} :price]}]}
;;     {:delivery
;;      [:delivered {:address [:street :zip {:country [:name {:neighbors ...}]}]}]}]

šŸ” 10
Joel23:02:29

Wow this is eery, I came here to ask a question about building some pathom code from malli. Are you working on something open-source and using pathom?

Panel23:02:59

Iā€™m also playing with pathom and malli, nothing worth publishing atm. But I think it might be useful to have more tool to transform malli schema into other form to be used to generate form/pathom/db schema/ā€¦ The actual part relevant to eql in the above code is not much.

Joel23:02:51

i had written code to pull data from a json request using malli, which i realized is too naiive (like nested data). But, also wanting to pass that to pathom to query as well.

Joel23:02:40

iā€™m putting a ā€œpathom like attributeā€ in the properties sections in malli schema to do the mapping, but the ::pco/output for pathom needs a more accurate mapping.

Joel23:02:14

is that what you are generating above?

Panel23:02:53

The code is generating the query, but it could be adapted to generate pathom output form easily. But those would be dependent on the underlying data store, so for example the above burger example could be split into many different resolver.