Projected local registries aren't preserved by m/form
Perhaps there's a way to achieve this that I've simply been missing. :)
Projection in the context of registries is a place where cycling out and back in through m/form breaks down.
user> (def local [:map {:registry {:looked/up 'int?}} [:x :looked/up]])
#'user/local
user> (m/validate (mu/get local :x) 123)
true
user> (m/validate (m/form (mu/get local :x)) 123)
Execution error (ExceptionInfo) at malli.core/-fail! (core.cljc:84).
:malli.core/invalid-schema {:schema :looked/up}
When using utilities like mu/get and similar (which I use to project types), the parent's registry is seemingly preserved... until serialization.
Whether part of m/form or a secondary utility, should it be possible to perform projections like these whilst preserving the registry?
I need this functionality for my use so I've built a custom version of mu/get that:
- constructs a composite registry using the local + passed-in registries
- grabs the subtree using
mu/getand the composite registry - walks the subtree using the composite registry to find any unresolved refs
- projects the topmost local registry to just contain keys used in the subtree
- uses
update-properties assocto put the projected registry on the new root
... but it seems like it should be possible to use the (-> ... mu/get m/form) example above without all of this messing around. :)
I see. You can get the accumulated registry with:
(-> local (mu/get :x) (m/options) :registry)
, but doesn't help much, as it's a CompoiteRegistry which has all the base schemas merged in it. I think we should:
- clearly separate the base registry with local (accumulated) registries, e.g. we could have a new
LocalRegistrytype, which contains the accumulation of all the local things - helpers to get the accumulated local registry entries from a schema
- have an option to
m/formto use the accumulated registry. - figure out a default for 3: maybe the top-level
m/formshould always have the accumulated registry, child forms not: this way them/formcould always be compiled back intoSchema, and the child forms would not contain duplicate/accumulated registry entries.
what do you think?
Thank you! This makes total sense to me and seems like what I've done.
I think on (4), the natural default is the one which round-trips with highest fidelity and can be slotted back into the parent. The behaviour you're suggesting seems to match that approach. Just to make sure I understand, it sounds like the default would be:
user>
(def top
[:map {:registry {::x 'int?}}
::x
[:k
[:schema {:registry {::y 'string?}}
[:tuple ::x ::y]]]])
;; ...
user> (m/form top)
;; identical to `top`, so includes the top-level registry
user> (m/form (m/get top ::x))
::x
user> (m/form (m/get top :k))
[:schema {:registry {::y 'string?}}
[:tuple ::x ::y]]
;; ^ these are useless without first being handed a registry that provides the ::x
;; that said, it doesn't create extra noise if it's now wrapped back in [:schema {:registry ...} <THIS>]