AvroBridge

dev.constructive.eo.avro.AvroBridge
See theAvroBridge companion object
final class AvroBridge[A, B] extends Optic[Array[Byte], BridgedBytes, A, B, Affine]

A two-version Avro migration bridge as an Affine-carried optic. Named from the OPTIC's point of view — it reads with the readCodec and writes with the writeCodec:

 AvroBridge.between[A, B]:
   Optic[Array[Byte],   // bytes READ (encoded under readCodec's schema)
         BridgedBytes,  // = Either[AvroFailure, Array[Byte]] — bytes WRITTEN (under writeCodec's schema), or the failure
         A,             // readFocus:  the value read out of the source bytes
         B,             // writeFocus: the value written back
         Affine]

to decodes the source bytes under the readCodec's schema — a Miss (so getOption is None) when they don't decode as an A. You supply the A ⇒ B migration as the function passed to the carrier-generic .modify (import dev.constructive.eo.optics.Optic.*); from re-encodes the resulting B under the writeCodec's schema. That re-encode can itself fail, and eo has no carrier whose from is fallible — so the outcome lives in the target type T = BridgedBytes = Either[AvroFailure, Array[Byte]] (this is the "BiAffine behaviour without a new carrier" we settled on; the generalized optic that would carry a fallible build natively is deferred).

'''Directed by design.''' The read side is A (the readCodec), the write side B (the writeCodec). AvroBridge.reverse swaps the two codecs to give the Optic[Array[Byte], BridgedBytes, B, A, Affine] going the other way. A fully symmetric encoding would be a BijectionIso Optic[BridgedBytes, BridgedBytes, A, B, Direct], but we deliberately favour directedness over that Iso and don't implement it.

Each side decodes / encodes under its OWN exact schema, so this is an explicit, user-driven migration between two model versions — NOT Avro's automatic writer→reader compatibility resolution (ResolvingDecoder; for that, see ConfluentWire.reader). The decode / encode go through the shared AvroCodec.decodeValue / AvroCodec.encodeValue helpers — no serialization path of its own.

Being an Optic[…, Affine], it picks up the capability-gated surface — .getOption, .modify, .replace, .andThen, … — from dev.constructive.eo.optics.Optic.

Attributes

Companion
object
Source
AvroBridge.scala
Graph
Supertypes
trait Optic[Array[Byte], BridgedBytes, A, B, Affine]
class Object
trait Matchable
class Any

Members list

Grouped members

Operations

inline def cross[C, D](o: Optic[BridgedBytes, Array[Byte], C, D, Affine])(using Accessor[Affine], ReverseAccessor[Affine]): Optic[B, A, C, D, Affine]

Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrier F. Flip self (it must be reversible — Accessor[F] and ReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThen that under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.

Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrier F. Flip self (it must be reversible — Accessor[F] and ReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThen that under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.

This is exactly self.reverse.andThen(that). The motivating case is ana.cross(cata): a Review (the unfold) crossed with a getter on the built S (the fold) — a (materializing) hylomorphism whose .get reads the folded value. When that sits on a different carrier (a Prism, a Fold, …), the cross-carrier cross overload below is selected instead.

Seam: that's source is self's T and its back-type is self's S.

Attributes

Inherited from:
Optic
Source
Optic.scala

Type members

Types

type X = (BridgedBytes, Array[Byte])

Structural leftover: Fst[X] is a AvroBridge.BridgedBytes (a Miss carries the failure as the T value), Snd[X] the source bytes (unused by from, which re-encodes the migrated B from scratch). Same X shape as Optional.

Structural leftover: Fst[X] is a AvroBridge.BridgedBytes (a Miss carries the failure as the T value), Snd[X] the source bytes (unused by from, which re-encodes the migrated B from scratch). Same X shape as Optional.

Attributes

Source
AvroBridge.scala

Value members

Concrete methods

def from(aff: Affine[X, B]): BridgedBytes

Re-encode the migrated B under the writeCodec's schema; a Miss passes its failure through as the Left of AvroBridge.BridgedBytes.

Re-encode the migrated B under the writeCodec's schema; a Miss passes its failure through as the Left of AvroBridge.BridgedBytes.

Attributes

Source
AvroBridge.scala
def reverse: AvroBridge[B, A]

The reverse migration — reads B under the (old) write schema, migrates B ⇒ A via .modify, and writes A under the (old) read schema. Just the two codecs swapped.

The reverse migration — reads B under the (old) write schema, migrates B ⇒ A via .modify, and writes A under the (old) read schema. Just the two codecs swapped.

Attributes

Source
AvroBridge.scala
def to(bytes: Array[Byte]): Affine[X, A]

Decode the source bytes under the readCodec's schema — a Miss when they don't decode as an A.

Decode the source bytes under the readCodec's schema — a Miss when they don't decode as an A.

Attributes

Source
AvroBridge.scala

Inherited methods

def andThen[C, IB, G[_, _]](inner: Optic[A, Unit, C, IB, G])(using rc: ReadCompose[Affine, G]): rc.Out[Array[Byte], C]

ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).

ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).

A trait member (not an extension in the companion) deliberately: once a receiver is statically one of the fused concrete classes, its andThen member overloads enter resolution and Scala 3 never falls back to extension methods when they all fail — the collapse must be in the member overload set to be reachable without an expected-type ascription.

Only the inner's T is pinned to Unit; its B stays free (IB) even though read-only inners always have B = Unit. That keeps this overload strictly LESS specific than the same-carrier andThen above (which accepts every argument this one does whenever B = Unit at the receiver), so a same-carrier read-only ∘ read-only call resolves unambiguously to the AssociativeFunctor path and this one fires exactly on the cross-seam cells the generic member cannot type.

Attributes

Inherited from:
Optic
Source
Optic.scala
inline def andThen[C, D](o: Optic[A, B, C, D, Affine]): Optic[Array[Byte], BridgedBytes, C, D, Affine]

Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.

Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.

Attributes

Example
case class Address(street: String)
case class Person(address: Address)
val streetLens = lens[Person](_.address).andThen(lens[Address](_.street))
Inherited from:
Optic
Source
Optic.scala