-
Notifications
You must be signed in to change notification settings - Fork 1
docs: MapStruct parity matrix + migration guide (audit deliverables) #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bd75e95
docs: MapStruct parity matrix + migration guide (audit deliverables)
eschizoid 46d20f8
docs: review-fleet fixes — working anchors, rendered generics, honest…
eschizoid 81b3b43
docs: Copilot round — parseable snippets, copy-pasteable import, self…
eschizoid 6a11b3d
docs: Copilot round two — consistent citations, unambiguous conversio…
eschizoid c5facc0
docs: second-round fleet fixes — truthful verifier claim, typed patch…
eschizoid 68953a9
docs: repair inline-code spans split across <-> tokens
eschizoid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Migrating from MapStruct to telescope | ||
|
|
||
| A working recipe for moving an existing MapStruct codebase to telescope **one mapper at a time** — no big-bang rewrite. | ||
| For the feature-by-feature reference (every MapStruct capability, the telescope idiom, and the honest limitations), see | ||
| the [parity matrix](mapstruct-parity.md). For the argument about _why_, run the head-to-head: | ||
| `./gradlew :examples:mapstruct-vs-telescope:test`. | ||
|
|
||
| ## The shape of the change | ||
|
|
||
| A MapStruct mapper is an annotated interface whose implementation is generated at compile time. A telescope mapper is a | ||
| **value** you build and hold: | ||
|
|
||
| ```java | ||
| // MapStruct — an annotated interface, implementation generated at compile time | ||
| @Mapper | ||
| public interface CustomerMapper { | ||
| @Mapping(source = "email", target = "contactEmail") | ||
| CustomerDto toDto(Customer customer); | ||
|
|
||
| Customer fromDto(CustomerDto dto); // the reverse direction is a second method | ||
| } | ||
| ``` | ||
|
|
||
| ```java | ||
| // telescope — one value, both directions | ||
| import static io.github.eschizoid.telescope.mapping.Mapping.to; | ||
|
|
||
| Mapper<Customer, CustomerDto> MAPPER = Telescope.mapper( | ||
| Customer.class, | ||
| CustomerDto.class, | ||
| to(Customer::email, CustomerDto::getContactEmail) | ||
| ); | ||
| // MAPPER.forward(customer) and MAPPER.backward(dto) — no second method | ||
| ``` | ||
|
|
||
| Strings become typed method references (the compiler checks them, the IDE refactors them), the reverse direction is | ||
| free, and the mapper can [explain and trace itself](../README.md) — which is also how you _verify_ each migration step | ||
| below. | ||
|
|
||
| ## Coexistence: migrate one mapper at a time | ||
|
|
||
| MapStruct and telescope run side by side indefinitely — they don't know about each other: | ||
|
|
||
| 1. Keep `mapstruct` + its processor on the build; add `telescope-core`. | ||
| 2. Pick one MapStruct mapper. Translate it (table below). Expose the telescope `Mapper<A, B>` wherever the old bean was | ||
| injected — in Spring / Quarkus, declare it as a `@Bean` / `@Produces` and the starter's `TelescopeMapperRegistry` | ||
| indexes it by `(sourceClass, targetClass)` automatically. | ||
| 3. Delete the MapStruct interface once its call sites are moved. Repeat. | ||
|
|
||
| Nothing forces order: leave the long tail on MapStruct as long as you like. | ||
|
|
||
| ## Translation table | ||
|
|
||
| | MapStruct | telescope | | ||
| | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ||
| | implicit same-name mapping | implicit too — `Telescope.mapper(A.class, B.class)` with zero rows deep-recurses by name | | ||
| | `@Mapping(source = "a", target = "b")` | `to(A::a, B::getB)` | | ||
| | `@Mapping(source = "x.y.z", target = "flat")` | `to(Telescope.of(A.class).field(A::x).field(X::y).field(Y::z), B::getFlat)` | | ||
| | `@Mapping(target = "t", ignore = true)` | `drop(A::t)` for a source-side field; a target-only field takes `constant(B::getT, …)` or the lenient `mapperForward(...)` | | ||
| | `@Mapping(expression = "java(...)")` | `to(A::src, B::getT, a -> compute(a), b -> invert(b))` — a typed function, not a string of Java | | ||
| | `@Mapping(constant = "fixed")` / `defaultValue` | `constant(B::getT, "fixed")` / `toOrElse(A::x, B::getX, fallback)` | | ||
| | type conversion (source `String` → target `LocalDate`) | `to(A::birthDate, B::getBirthDate, LocalDate::parse, LocalDate::toString)` — forward parses, backward formats | | ||
| | nested type reuse / `@Mapper(uses = …)` | `via(A::address, B::getAddress, addressMapper)` | | ||
| | `@AfterMapping` / `@BeforeMapping` | `mapper.afterForward((src, dst) -> …)` / `mapper.beforeForward(…)` — returns a new immutable mapper | | ||
| | `void update(@MappingTarget E e, D d)` | `mapper.into(entity, dto)` (silent-skip on missing setters, same semantics) / `mapper.patch(…)` | | ||
| | `CustomerDto toDto(Customer c, Address a)` | `Telescope.merge(CustomerDto.class, auto(Customer.class), from(Address::city, CustomerDto::getCity))` | | ||
| | `unmappedTargetPolicy = ERROR` | free — strict `mapper(...)` refuses unmapped fields at construction (and at **compile time** with `telescope-codegen` on the processor path) | | ||
| | `unmappedTargetPolicy = WARN` (the default) | `Telescope.mapperForward(A.class, B.class)` — lenient one-way like MapStruct's default at runtime (silently, i.e. IGNORE; for a build-time warning instead, keep strict `mapper(...)` and set `-Atelescope.verify=warn`) | | ||
| | `componentModel = "spring"/"cdi"` | plain `@Bean` / `@Produces Mapper<A, B>` + the starter's `TelescopeMapperRegistry` | | ||
| | generated `…MapperImpl` you read to debug | `mapper.explain()` / `mapper.trace(input)` / flip a log level — [introspection](../README.md) | | ||
|
|
||
| Every mapping row above is expanded — with evidence and limitations — in the [parity matrix](mapstruct-parity.md). | ||
|
|
||
| ## Verify each step (don't trust the diff, assert it) | ||
|
|
||
| telescope's introspection makes each migrated mapper self-checking: | ||
|
|
||
| ```java | ||
| import io.github.eschizoid.telescope.introspection.OpticNode.Mapped; | ||
|
|
||
| // the migrated mapper maps everything the old one did — nothing silently dropped | ||
| assertThat(mapper.explain().skipped()).isEmpty(); | ||
|
|
||
| // the rename you carried over is real, enumerable data | ||
| assertThat(mapper.explain().mapped()).contains(new Mapped("email", "contactEmail")); | ||
|
eschizoid marked this conversation as resolved.
|
||
|
|
||
| // and for one golden input, old and new agree | ||
| assertThat(mapper.forward(sample)).isEqualTo(oldMapStructMapper.toDto(sample)); | ||
| ``` | ||
|
|
||
| That last equality assertion — run once per migrated mapper against the still-present MapStruct implementation — is the | ||
| cheapest possible safety net, and you delete it together with the old interface. | ||
|
|
||
| ## Gotchas worth knowing up front | ||
|
|
||
| - **Strict vs lenient is a choice, not a default fight.** `Telescope.mapper(...)` is strict (bidirectional, refuses | ||
| unmapped fields); `Telescope.mapperForward(...)` is lenient one-way and matches MapStruct's default. Migrating a | ||
| mapper often _surfaces_ fields MapStruct was silently nulling — that's the point, not a regression. | ||
| - **Multiple source parameters** go through `Telescope.merge(...)` with a `Sources` bag — the source-class check happens | ||
| at `forward(...)` time, not at the method signature (see the matrix row for details). | ||
| - **Row-group reuse** (`MapperBuilder.inherit`) currently applies only across mappers of the **same** (source, target) | ||
| pair — a group inherited into a different pair is silently dropped | ||
| ([#223](https://github.com/eschizoid/telescope/issues/223)). | ||
| - **Multi-constructor immutables** (MapStruct's `@Default`) aren't disambiguated — records and single-constructor | ||
| classes are the supported shapes. | ||
| - **Hot loops:** the runtime path is reflective (fast enough outside hot paths); annotate the pair with | ||
| `@Focus`/`@Bridge` and the codegen path is reflection-free. | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.