Skip to content

feat: magic_map_scope! — nested funnelling for foreign→foreign mappings - #2

Merged
rsansores merged 1 commit into
mainfrom
feat/magic-map-scope
Aug 9, 2026
Merged

rsansores merged 1 commit into
mainfrom
feat/magic-map-scope

Conversation

@rsansores

Copy link
Copy Markdown
Owner

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.

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.
@rsansores
rsansores merged commit fba6c3f into main Aug 9, 2026
1 check passed
@rsansores
rsansores deleted the feat/magic-map-scope branch August 9, 2026 21:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant