Open jeroenvandijk opened 2 years ago
I think a generic solution to have a Schema wrapper that allows one to create the current snapshot of the Schema
instance programmatically. This would work with all Schema types, not just with :multi
.
Did a prototype, works like this:
(def schema* (atom :int))
(def mut (mu/mutable #(m/schema @schema*)))
(m/form [:map [:mut mut]])
; => [:map [:mut :int]]
(reset! schema* [:enum "so" "mutable"])
(m/form [:map [:mut mut]])
; => [:map [:mut [:enum "so" "mutable"]]]
with multimethods:
(defmulti my-schema :type)
(defmethod my-schema :user [_]
[:map
[:type [:= :user]]
[:name :string]])
(def multi
(mu/mutable
#(m/into-schema
:multi
{:dispatch :type}
(map (fn [[type f]] [type (f {:type type})]) (methods my-schema)))))
multi
;[:multi {:dispatch :type}
; [:user [:map
; [:type [:= :user]]
; [:name :string]]]]
(m/validate multi {:type :user, :name "kikka"})
; => true
(m/validate multi {:type :fruit, :taste "sweet"})
; => false
(defmethod my-schema :fruit [_]
[:map
[:type [:= :fruit]]
[:taste [:enum "sweet" "sour"]]])
multi
;[:multi
; {:dispatch :type}
; [:fruit [:map
; [:type [:= :fruit]]
; [:taste [:enum "sweet" "sour"]]]]
; [:user [:map
; [:type [:= :user]]
; [:name :string]]]]
(m/validate multi {:type :user, :name "kikka"})
; => true
(m/validate multi {:type :fruit, :taste "sweet"})
; => true
the Schema
instance is mutable itself, with no caching enabled, so calling (m/validate multi ...)
always uses the latest value. Calling (m/validator multi)
will create a immutable snapshot with the current value.
would think work with you?
... with suger:
(defn multimethod-schema [mm]
(let [dispatch (.dispatchFn mm)]
(m/into-schema
:multi
{:dispatch dispatch}
(map (fn [[type f]] [type (f {dispatch type})]) (methods mm)))))
in use:
(mu/multimethod-schema my-schema)
;[:multi
; {:dispatch :type}
; [:fruit [:map
; [:type [:= :fruit]]
; [:taste [:enum "sweet" "sour"]]]]
; [:user [:map
; [:type [:= :user]]
; [:name :string]]]]
;; the previous example
(def multi (mu/mutable #(mu/multimethod-schema my-schema)))
@ikitommi Yeah I think this would work. Relying on mu/mutable
also makes it explicit that the implementation relies on state. In development I could use mu/mutable
and in production, for better performance, I could decide to use the initial schema as the final schema (assuming I'm sure all multimethod extensions are loaded).
In
clojure.spec
multi-spec allows to dynamically extend specs. This gives a lot of flexibility. For example, it allows to create a plugin system where definitions are loaded at a later time, in a different part of the code. Quoting spec's documentation:E.g. consider the small repl session below, it shows that a
spec
can be extended and redefined at runtime.In Malli there is the
:multi
schema. Currently this supports loading predefined keys lazily. It does not support a clean way to redefine, let alone add or remove keys at a later stage. In a Slack discussion with @ikitommi it was suggested to add a:methods
option to dynamically define the keys. I tried this in a seperate PR, but this only adds keys lazily and doesn't allow to redefine the keys.If I have to think of an implementation similar to
multispec
it has to be with a dynamic entry parser (not just lazy) and maybeadd-watch
on the multimethod var. Theclojure.spec
implementation looks different though. And after some experimentation I noticed that adding adefmethod
doesn't trigger a var change, soadd-watch
wouldn't work here.[1] https://clojurians.slack.com/archives/CLDK6MFMK/p1669045351866979