Repository navigation
feat: magic_map_scope! — nested funnelling for foreign→foreign mappings - #2
Merged
Merged
Conversation
The fn form exists because `impl MapFrom<Src> for Dest` is only legal in a crate that owns one of the two types. But a mapping's *fields* funnelled through `MapFrom` as well, and the fn form leaves no impl behind — so a nested field whose own mapping was also foreign→foreign had nothing to resolve against. `Vec<Address>` → `Vec<AddressResponse>` needs `AddressResponse: MapFrom<Address>`, and that impl cannot exist anywhere. Mapper crates owning neither side were writing the recursion by hand, per field and per collection. `magic_map_scope!()` plants a trait in the calling crate. The orphan rule is satisfied by a local trait just as well as by a local type, so `impl LocalMapFrom<Address> for AddressResponse` is legal there even with both types foreign, and the fn form now emits one for every mapping it declares. Nested pairs, `Option`, `Vec` and the `..Default::default()` adaptor compose again — which is what lets a services layer hold the mappers between a `*_db` crate and a `*_dtos` crate that know nothing about each other. Nothing else is ever registered. Field resolution probes two tiers by autoref and the fn form emits CONCRETE tier-1 impls, one per declared pair, so a leaf pair has no tier-1 candidate and probing derefs to the tier-2 blanket over `MapFrom`. Built-in leaves, `map_identity!`/`map_display!`/`map_parse!` declarations, hand-written impls and generic wrappers like a `Patch<T>` update field are all found without being named. A *blanket* tier 1 would instead match every pair structurally and report the unsatisfied bound rather than falling through — method probing selects by receiver shape and never retries a lower tier — which is why the obvious spelling does not work and this one does. Nor can the local trait simply forward to `MapFrom`: that blanket overlaps the per-pair impls and coherence rejects it, because either upstream crate could add the conflicting impl later. Both compiler errors are quoted in the README so the next person does not re-derive them. Breaking: a crate using the fn form must call `magic_map_scope!()` in its crate root. It takes no arguments. Impl-form-only crates are unaffected. Verified against a workspace with 38 leaf declarations across three crates: every one resolved with a bare scope call, including `Patch<T>`. Tests cover the nested foreign→foreign case, None/empty propagation, custom and generic leaves, and leaf fallthrough asserted alongside a declared pair in the same scope, so it is a real fallthrough and not tier 1 being absent. `compat/scope-downstream` is a crate with no features of its own that maps every built-in leaf: it guards the cfg-leakage class of bug, where an emitted `#[cfg(feature = "uuid")]` is evaluated against the *calling* crate and silently drops every gated leaf. magic_map's own test crate cannot catch that, because its features are this crate's. CI builds it. Known edge, documented: a declared pair nested inside a *custom* generic wrapper (`Patch<Address>` → `Patch<AddressResponse>`, unlike `Patch<String>` → `Patch<String>`) has no tier-1 candidate and wants an explicit override. `Option` and `Vec` over declared pairs are emitted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The fn form exists because
impl MapFrom<Src> for Destis only legal in a crate that owns one of the two types. But a mapping's fields funnelled throughMapFromas well, and the fn form leaves no impl behind — so a nested field whose own mapping was also foreign→foreign had nothing to resolve against.Vec<Address>→Vec<AddressResponse>needsAddressResponse: MapFrom<Address>, and that impl cannot exist anywhere. Mapper crates owning neither side were writing the recursion by hand, per field and per collection.magic_map_scope!()plants a trait in the calling crate. The orphan rule is satisfied by a local trait just as well as by a local type, soimpl LocalMapFrom<Address> for AddressResponseis legal there even with both types foreign, and the fn form now emits one for every mapping it declares. Nested pairs,Option,Vecand the..Default::default()adaptor compose again — which is what lets a services layer hold the mappers between a*_dbcrate and a*_dtoscrate that know nothing about each other.Nothing else is ever registered. Field resolution probes two tiers by autoref and the fn form emits CONCRETE tier-1 impls, one per declared pair, so a leaf pair has no tier-1 candidate and probing derefs to the tier-2 blanket over
MapFrom. Built-in leaves,map_identity!/map_display!/map_parse!declarations, hand-written impls and generic wrappers like aPatch<T>update field are all found without being named. A blanket tier 1 would instead match every pair structurally and report the unsatisfied bound rather than falling through — method probing selects by receiver shape and never retries a lower tier — which is why the obvious spelling does not work and this one does.Nor can the local trait simply forward to
MapFrom: that blanket overlaps the per-pair impls and coherence rejects it, because either upstream crate could add the conflicting impl later. Both compiler errors are quoted in the README so the next person does not re-derive them.Breaking: a crate using the fn form must call
magic_map_scope!()in its crate root. It takes no arguments. Impl-form-only crates are unaffected.Verified against a workspace with 38 leaf declarations across three crates: every one resolved with a bare scope call, including
Patch<T>. Tests cover the nested foreign→foreign case, None/empty propagation, custom and generic leaves, and leaf fallthrough asserted alongside a declared pair in the same scope, so it is a real fallthrough and not tier 1 being absent.compat/scope-downstreamis a crate with no features of its own that maps every built-in leaf: it guards the cfg-leakage class of bug, where an emitted#[cfg(feature = "uuid")]is evaluated against the calling crate and silently drops every gated leaf. magic_map's own test crate cannot catch that, because its features are this crate's. CI builds it.Known edge, documented: a declared pair nested inside a custom generic wrapper (
Patch<Address>→Patch<AddressResponse>, unlikePatch<String>→Patch<String>) has no tier-1 candidate and wants an explicit override.OptionandVecover declared pairs are emitted.