malli

https://github.com/metosin/malli :malli:
rutledgepaulv 2020-12-05T15:33:33.174Z

is there a recommended way to write a function that will dispatch to an implementation according to which of several schemas match my input arguments? I can construct my own of course that just does a linear search, and i could create a multi schema to compose them together (though i would need to use a compound discriminator for my case). I guess what i'm wondering is if there's a way to do pattern matching using malli schemas, but preferably in an open way like multimethods so i can just accrete new cases instead of modifying a case expression.

rutledgepaulv 2020-12-05T15:35:48.174900Z

maybe i'm just rubbing up against open arbitrary predicate dispatch at that point. hm

rutledgepaulv 2020-12-05T15:37:09.175900Z

perhaps i could add an optional attribute on each schema (in my case they are open maps so this is fine) and supply a :default attribute. then i can run a default transformer on my inbound value to inject a "type" and from there just use regular multimethods to dispatch on that attribute.

rutledgepaulv 2020-12-05T15:57:03.177500Z

yeah that works. sorta rough and not sure the macro is a good idea but in case anyone is curious:

rutledgepaulv 2020-12-05T15:57:07.177800Z

(defn dispatchable [dispatch-key schema]
  (mu/update-properties
    schema
    (fn [props]
      (let [dispatch-decoder (fn [value] (with-meta value {:dispatch dispatch-key}))]
        (assoc-in props [:decode/dispatch] {:leave dispatch-decoder})))))

(defn dispatch-key [schema value]
  (some-> (m/decode schema value (mt/transformer {:name :dispatch})) meta :dispatch))

(defmacro defdispatchable [symbol & body]
  `(def ~symbol (dispatchable ~(name symbol) (do ~@body))))

(defdispatchable token-file-auth
  [:map [:tokenFile :string]])

(defdispatchable client-key-auth
  [:map
   [:client-key-data :string]
   [:client-certificate-data :string]])

(def combined
  [:or token-file-auth client-key-auth])

(defmulti handle-auth (fn [context] (dispatch-key combined context)))

(defmethod handle-auth "token-file-auth" [context]
  (println "token file!"))

(defmethod handle-auth "client-key-auth" [context]
  (println "client key!"))

rutledgepaulv 2020-12-05T15:57:19.178100Z

(handle-auth {:tokenFile "st"})
token file!
=> nil
(handle-auth {:client-key-data "st" 
              :client-certificate-data "sfd"})
client key!

rutledgepaulv 2020-12-05T16:20:31.178900Z

Actually, i think this is much better:

rutledgepaulv 2020-12-05T16:20:34.179300Z

(def schema-resolver-transformer
  (mt/transformer
    {:default-decoder
     {:compile (fn [schema _]
                 (fn [value]
                   (if (instance? IObj value)
                     (vary-meta value assoc :resolved schema)
                     value)))}}))

(defn resolve-schema [schema value]
  (some-> (m/decode schema value schema-resolver-transformer) (meta) :resolved))

(def token-file-auth
  [:map {:dispatch :token-file}
   [:tokenFile :string]])

(def client-key-auth
  [:map {:dispatch :client-key}
   [:client-key-data :string]
   [:client-certificate-data :string]])

(def combined
  [:or token-file-auth client-key-auth])

(defn dispatch-fn [context]
  (let [schema (resolve-schema combined context)]
    (:dispatch (m/properties schema))))

(defmulti handle-auth #'dispatch-fn)

(defmethod handle-auth :token-file [context]
  (println "token file!"))

(defmethod handle-auth :client-key [context]
  (println "client key!"))

rutledgepaulv 2020-12-05T16:30:50.183700Z

Tommi, let me know if there's a better way to do this ^. Providing a way to resolve the concrete schema that is used for each node in a tree of data allows me to put useful data into the schema properties and recover that data for a given value so that i can act on it (like dispatching to different functions in my code based on which schema matched). Here i am attaching the schema to the data as metadata (which makes sense but will not work for values that don't implement IObj)

ikitommi 2020-12-05T18:08:17.186300Z

@sfyire @borkdude I think one needs to add extra entry into :config-paths into .clj-kondo/config.edn. I have the following:

✗ cat .clj-kondo/config.edn
{:config-paths ["configs/malli"]}
forgot all about that, will add it to README if that is the required glue here.

ikitommi 2020-12-05T18:08:44.186700Z

would be great if that was not needed and it would just work.

ikitommi 2020-12-05T18:09:19.187200Z

e.g. with that, clj-kondo pics up the emitted file, looking something like this:

✗ cat .clj-kondo/configs/malli/config.edn
{:lint-as #:malli.schema{defn schema.core/defn}
 :linters {:type-mismatch {:namespaces {user {square {:arities {1 {:args [:int], :ret :nat-int}}}
                                              plus {:arities {1 {:args [:int], :ret :int}
                                                              2 {:args [:int :int], :ret :int}}}}}}}}

ikitommi 2020-12-05T18:31:24.195400Z

@rutledgepaulv your second options looks interesting, tagging values. I most likely would do it like that. One problem I see with this approach is that the meta-data can get out-of-sync: if your say (dissoc data :tokenFile) , it still is tagged as :token-file, but it not anymore valid against that schema. Might not be a problem if you just use it once, but relying of :resolved meta in general has not guarantees. The Schema parsing is currently WIP (first release coming soon), with that, you will have a variant to :or (and :alt), which will have named branches, like spec does, so you can say ~about this:

(def token-file-auth
  [:map
   [:tokenFile :string]])

(def client-key-auth
  [:map
   [:client-key-data :string]
   [:client-certificate-data :string]])

;; using named :or variant
(def combined
  [:or*
    [:token-file token-file-auth]
    [:client-key client-key-auth]])

(m/parse combined {:token-file "kikka"})
; => #Branch{:key :token-file 
             :value {:token-file "kikka"}}

rutledgepaulv 2020-12-05T18:34:15.196800Z

thanks! the schema parsing stuff looks like that would address my issue too

ikitommi 2020-12-05T18:34:24.197100Z

you could also use m/encode and return a tuple yourself, with both the schema/name and the value.

rutledgepaulv 2020-12-05T18:35:14.198Z

yeah. in my case i only need to know the concrete version of the top level schema so tuples would be fine

👍 1