AvroPrism

dev.constructive.eo.avro.AvroPrism
See theAvroPrism companion object
final class AvroPrism[A] extends Optic[Array[Byte], Array[Byte], A, A, Affine], Dynamic

Optic from the Avro BINARY WIRE FORM to a native type A — the wire bytes are the default carrier:

 AvroPrism[A] <: Optic[Array[Byte], Array[Byte], A, A, Affine]
 type X = (Array[Byte], (Array[Byte], BinarySpan))

Reads locate the focused field's byte span via AvroBinaryCursor and decode only that slice; writes re-encode the focus and splice it in place (three arraycopys, union branch index re-synthesised). The ROOT object is never materialised — neither as a case class nor as a generic record; a record-SHAPED focus is materialised only as that branch's own org.apache.avro.generic.IndexedRecord during the slice decode / re-encode. Mirrors dev.constructive.eo.jsoniter.JsoniterPrism shape-for-shape:

  • Fst[X] = Array[Byte] — original payload; Miss carries it for pass-through (parse failure, path miss, union-branch mismatch, decode failure, unsupported index step).
  • Snd[X] = (Array[Byte], BinarySpan) — payload + located span, so from can splice.

The whole capability-gated extension surface lights up on the bytes — .getOption, .modify, .replace, .foldMap, .andThen, … Drill with the same macro sugar as ever (.field(_.x) / .fields(...) / .union[B] / .at(i) / .each / Dynamic field selection).

'''Field navigation honours the SCHEMA field name (issue #35).''' .field(_.x) (and .fields, selectDynamic, the traversal siblings) resolve the case-class field x to whatever schema field the codec actually emitted for it — under any name transform (kindlings snake / kebab / custom transformFieldNames, or a vulcan per-field override map) — by DECLARATION POSITION: the i-th case field maps to the i-th schema field, read back out of the cached schema at construction time (zero per-operation cost). The rare hand-written codec whose schema field ORDER diverges from declaration order needs AvroPrism.fieldNamed("schema_name") to navigate by the explicit schema name instead. Map keys are data, not schema-named fields, and keep their literal key.

Two sibling surfaces, one mechanism each (deliberately NOT duplicated here):

  • record — the IndexedRecord-carried optic (AvroRecordPrism) with the Ior-bearing diagnostic surface (get / modify / place / transfer + *Unsafe) over IndexedRecord | Array[Byte] | String input. Use it when you hold parsed records or need accumulated AvroFailure diagnostics.
  • sliceBytes / graftBytes — the encoded-fragment surface for hashing / shipping / splicing a field's raw encoding across payloads without decoding the focus at all.

Storage decomposition: an AvroPrism[A] holds an AvroFocus (Leaf vs Fields) and a cached root schema; the byte walk uses the focus's path, the slice decode uses its codec.

'''Laws & preconditions''' (normative):

  • The Optional laws hold '''up to canonical re-encoding of the focused slice''': modify(identity) re-encodes the focus, so a payload whose focused slice used non-canonical (but spec-legal) encodings — non-minimal varints, byte-sized array blocks — comes back canonicalised. Byte-for-byte identity holds for payloads from conformant writers (apache-avro's own encoders included); put-get and put-put hold unconditionally.
  • '''Writes require a decodable current focus''': the Affine to decodes eagerly, so .replace onto a span whose current value doesn't decode as A — or a .union[B] focus sitting on a different runtime branch — is a Miss pass-through. graftBytes is the decode-free write (and the only one that can SWITCH union branches).
  • '''The payload must be encoded under exactly this prism's reader schema.''' The byte walk performs no writer/reader schema resolution: structurally drifted payloads Miss silently, and a same-typed field REORDER between writer and reader is undetectable from the bytes — the walk reads the wrong field with full confidence. Confluent-framed payloads are handled by composing ConfluentWire.confluent (a byte Prism that strips the header, resolves the writer schema, and fingerprint-gates) BEFORE this optic — confluent.andThen(thisWalk); past a fingerprint mismatch a mixed-schema topic still needs a resolving decode (the record face with the right schema per payload).
  • Dynamic field sugar is shadowed by real members: an Avro field named like a member of this class (record, field, at, union, each, fields, …) must be drilled with the explicit .field(_.record) form.

Attributes

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

Members list

Grouped members

Operations

inline def cross[C, D](o: Optic[Array[Byte], Array[Byte], C, D, Affine])(using Accessor[Affine], ReverseAccessor[Affine]): Optic[A, 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 = (Array[Byte], (Array[Byte], BinarySpan))

Structural leftover: Fst[X] is the original payload (Miss pass-through), Snd[X] the payload plus the located span so from can splice without re-walking.

Structural leftover: Fst[X] is the original payload (Miss pass-through), Snd[X] the payload plus the located span so from can splice without re-walking.

Attributes

Source
AvroPrism.scala

Value members

Concrete methods

transparent inline def at(i: Int): Any
Extension method from AvroPrism

.at(i) — drill into the i-th array element / map entry.

.at(i) — drill into the i-th array element / map entry.

Attributes

Source
AvroPrism.scala
transparent inline def each: Any
Extension method from AvroPrism

.each — split into an AvroTraversal over the iterated array.

.each — split into an AvroTraversal over the iterated array.

Attributes

Source
AvroPrism.scala
transparent inline def field[B](inline selector: A => B)(using codecB: AvroCodec[B]): AvroPrism[B]
Extension method from AvroPrism

.field(_.x) — drill via selector lambda.

.field(_.x) — drill via selector lambda.

Attributes

Source
AvroPrism.scala
def fieldNamed[B](schemaName: String)(using codecB: AvroCodec[B]): AvroPrism[B]
Extension method from AvroPrism

.fieldNamed[B]("schema_name") — drill by the EXPLICIT schema field name, bypassing position resolution. The escape hatch (issue #35) for a hand-written codec whose schema field order diverges from case-class declaration order; the common (derived / order-preserving) codecs need .field(_.x) instead, which resolves the name for you.

.fieldNamed[B]("schema_name") — drill by the EXPLICIT schema field name, bypassing position resolution. The escape hatch (issue #35) for a hand-written codec whose schema field order diverges from case-class declaration order; the common (derived / order-preserving) codecs need .field(_.x) instead, which resolves the name for you.

Attributes

Source
AvroPrism.scala
transparent inline def fields(inline selectors: A => Any*): Any
Extension method from AvroPrism

.fields(_.a, _.b, ...) — focus a NamedTuple over selected fields.

.fields(_.a, _.b, ...) — focus a NamedTuple over selected fields.

Attributes

Source
AvroPrism.scala
def from(aff: Affine[X, A]): Array[Byte]

Re-encode the focus and splice it over the located span; a Miss passes the payload through unchanged.

Re-encode the focus and splice it over the located span; a Miss passes the payload through unchanged.

Attributes

Source
AvroPrism.scala
def graftBytes(bytes: Array[Byte], fragment: Array[Byte]): Ior[Chain[AvroFailure], Array[Byte]]

Graft an encoded fragment into a binary payload at the focused field — splice bytes in place of the field's current encoding, decode-free: prefix copy + (when the prism focuses a union branch: the zigzag-encoded index of the prism's focused branch, re-synthesised from the path — the payload's current branch is discarded, so grafting can SWITCH branches) + the fragment bytes + suffix copy.

Graft an encoded fragment into a binary payload at the focused field — splice bytes in place of the field's current encoding, decode-free: prefix copy + (when the prism focuses a union branch: the zigzag-encoded index of the prism's focused branch, re-synthesised from the path — the payload's current branch is discarded, so grafting can SWITCH branches) + the fragment bytes + suffix copy.

'''NO SCHEMA VALIDATION.''' The fragment bytes are spliced verbatim — this method does NOT check that fragment is a valid encoding of the focused field's schema. A fragment encoded under a different (or drifted) schema produces a payload that is silently corrupt until something decodes it. The caller owns schema-fingerprint checks (compare the writer schema / Confluent schema id of the fragment's source against the receiving field's schema) BEFORE grafting.

Attributes

Source
AvroPrism.scala
def graftBytesUnsafe(bytes: Array[Byte], fragment: Array[Byte]): Array[Byte]

Silent counterpart to graftBytes — input bytes pass through unchanged on any failure. Same NO-SCHEMA-VALIDATION caveat as graftBytes.

Silent counterpart to graftBytes — input bytes pass through unchanged on any failure. Same NO-SCHEMA-VALIDATION caveat as graftBytes.

Attributes

Source
AvroPrism.scala

The IndexedRecord-carried counterpart of this prism — same focus, but Optic[IndexedRecord, IndexedRecord, A, A, Affine] plus the Ior-bearing diagnostic surface. Drill here on the byte prism, then flip at the end:

The IndexedRecord-carried counterpart of this prism — same focus, but Optic[IndexedRecord, IndexedRecord, A, A, Affine] plus the Ior-bearing diagnostic surface. Drill here on the byte prism, then flip at the end:

 codecPrism[Person].field(_.name).record.modify(_.toUpperCase)(record)

Attributes

Source
AvroPrism.scala
transparent inline def selectDynamic(inline name: String): Any

Dynamic field sugar — codecPrism[Person].name lowers to codecPrism[Person].field(_.name) via the macro. Shadowed by real members (record, field, at, …); use the explicit .field(_.x) form for an Avro field named like one of them.

Dynamic field sugar — codecPrism[Person].name lowers to codecPrism[Person].field(_.name) via the macro. Shadowed by real members (record, field, at, …); use the explicit .field(_.x) form for an Avro field named like one of them.

Attributes

Source
AvroPrism.scala
def sliceBytes(bytes: Array[Byte]): Ior[Chain[AvroFailure], AvroFragment]

Slice the focused field's encoded value bytes out of a binary payload.

Slice the focused field's encoded value bytes out of a binary payload.

The returned AvroFragment carries the value bytes (union branch index STRIPPED when the prism focuses a union branch), the resolved field / branch schema, and the branch ordinal. The runtime branch must match the prism's focused branch — a payload sitting on a different branch surfaces AvroFailure.UnionResolutionFailed.

Array-index steps are unsupported (AvroFailure.UnsupportedSpanStep); for a .fields(...) prism the span addressed is the PARENT record enclosing the selected fields (the selected fields themselves are not contiguous).

Attributes

Source
AvroPrism.scala
def sliceBytesUnsafe(bytes: Array[Byte]): Option[AvroFragment]

Silent counterpart to sliceBytesNone on any failure.

Silent counterpart to sliceBytesNone on any failure.

Attributes

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

Locate + slice-decode the focus — Hit carries the span for from, Miss the payload.

Locate + slice-decode the focus — Hit carries the span for from, Miss the payload.

Attributes

Source
AvroPrism.scala
transparent inline def union[Branch]: Any
Extension method from AvroPrism

.union[Branch] — drill into a union alternative by branch type.

.union[Branch] — drill into a union alternative by branch type.

Attributes

Source
AvroPrism.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, A, C, D, Affine]): Optic[Array[Byte], Array[Byte], 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