Optics: Boundary Mapping Journey

What We'll Learn

  • Multi-edit and sparse updates: several edits, one operation, all errors at once
  • ValidatedPrism: parse-don't-validate as an optic, with both round-trip laws
  • @GenerateMapping: the whole domain ↔ DTO boundary derived from a spec interface
  • Located errors end to end: leaves, nesting, renames, and the sparse PATCH sibling
  • The edge cases: a null, a list index, a left-out field, a record's invariant, and a PATCH bean's default

Duration: ~50 minutes | Tutorials: 4 (T24-T27) | Exercises: 19

Where This Fits in the Bigger Picture

This journey is the hands-on lane for the Mapping at the Boundary chapter. Tutorial 24 builds the update-side machinery by hand (Edits.combine / Edits.accumulate), Tutorial 25 builds the leaf every fallible correspondence rests on (ValidatedPrism), and Tutorial 26 lets the processor derive the whole boundary and proves it lawful. Tutorial 27 takes it to the edge cases a real request brings. The capstone then shows the same machinery at full scale.

Prerequisites: Optics: Lens & Prism Journey; the accumulating-assembly exercises in the Error Handling Journey help with Tutorials 25-27.

Journey Overview

A service boundary has two directions and two failure styles: outbound rendering that cannot fail, and inbound parsing that should report every problem, located. This journey builds that boundary from its parts, then generates it:

T24  Edits.accumulate    the hand-written fold
 │
 ▼
T25  ValidatedPrism      the fallible leaf
 │
 ▼
T26  @GenerateMapping    the derived boundary
 │
 ▼
T27  edge cases          nulls, list indexes, invariants, PATCH defaults

Tutorial 24: Multi-Edit and Sparse Updates (~12 minutes)

File: Tutorial24_MultiEdit.java | Exercises: 5

Apply N independent edits at different paths in one reusable operation, including the sparse, all-errors-at-once REST PATCH shape.

What you'll learn:

  • Folding pure edits into one reusable Update<S> with Edits.combine
  • Sparse updates: the …IfPresent factories treat null as "leave it alone"
  • The validated PATCH: Edits.accumulate reports all located failures at once
  • Why a fallible edit cannot slip into combine (compile-time purity)

Key insight: validation is source-independent and runs first; the writes run as one fold only if everything validated.


Tutorial 25: ValidatedPrism (~10 minutes)

File: Tutorial25_ValidatedPrism.java | Exercises: 3

The smart-constructor optic: a Prism whose match says why not, and all the reasons at once.

What you'll learn:

  • ValidatedPrism.of(parse, build): a fallible, accumulating parse and a total build
  • Lifting a plain prism with a reason via fromPrism
  • Nesting short-circuits; sibling fields accumulate through Validated.fields()
  • Verifying both round-trip laws with ValidatedPrismLaws

Key insight: the section law forbids a normalising build; the prism's parse is exactly the leaf shape the mapper and the Edits builder consume.


Tutorial 26: Record Mapping (~12 minutes)

File: Tutorial26_RecordMapping.java | Exercises: 5

The boundary, generated: @GenerateMapping derives a total build and an accumulating, located parse from a spec interface (the specs live in org.higherkindedj.example.tutorials.mapping, main sources, where the processor runs).

What you'll learn:

  • Calling the generated Impl, bound once in the calling class: build is total, parse returns Validated<NonEmptyList<FieldError>, Domain>
  • Reading located errors: stock codec messages, a nested spec's guest.email path, declaration order
  • Law-checking a mapping with one MappingLaws call
  • The sparse PATCH sibling: UpdateSpec, null-as-absent, same leaf vocabulary

Key insight: everything Tutorials 24 and 25 built by hand is what the processor derives, and the laws prove the derivation honest.


Tutorial 27: Boundary Edge Cases (~15 minutes)

File: Tutorial27_BoundaryEdgeCases.java | Exercises: 6

A real request is rarely just a bad value. It leaves a field out, sends a list with one bad element, breaks a rule that spans two fields, or arrives as a PATCH bean that fills in a value nobody sent. Each exercise asks where that request lands. Its specs sit beside Tutorial 26's.

What you'll learn:

  • A null field on the wire is a located error, beside every other error
  • A list element is located by its index
  • @OptionalBridge declares, per component, that a null means absent
  • A record's constructor refusal becomes an error at the record's path
  • A PATCH bean's default reads as sent, and how a law catches it
ExerciseThe requestThe question
1A booking with no id, whose guest has no emailWhere is each null reported, and with what message?
2A party whose first guest has no name, and whose second has a bad emailHow does the path say which guest?
3A room request that leaves its note outWhat does @OptionalBridge make of the null?
4A stay whose departure is not after its arrivalWhere does the constructor's refusal land?
5A PATCH that sends nothingHow do we check that it changes nothing?
DiagnosticThe same PATCH, on a bean whose schema said default: falseWhy does the team's law pass, and which sample makes it fail?

Key insight: parse and updateFrom return every edge case here as a value, never a thrown exception. The odd one out, a PATCH bean's default, comes back valid and wrong; only a law, run with a sample that differs from the default, catches it.


See Also


Previous: Optics: Batching & Coupled Updates Next: Expression: ForState