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.
maybe i'm just rubbing up against open arbitrary predicate dispatch at that point. hm
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.
yeah that works. sorta rough and not sure the macro is a good idea but in case anyone is curious:
(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!"))
(handle-auth {:tokenFile "st"})
token file!
=> nil
(handle-auth {:client-key-data "st"
:client-certificate-data "sfd"})
client key!
Actually, i think this is much better:
(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!"))
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)
@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.would be great if that was not needed and it would just work.
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}}}}}}}}
https://github.com/metosin/malli/commit/da45eceaea3e5b34426027bf7836e7cbe841cc97
@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"}}
thanks! the schema parsing stuff looks like that would address my issue too
you could also use m/encode
and return a tuple yourself, with both the schema/name and the value.
yeah. in my case i only need to know the concrete version of the top level schema so tuples would be fine