dev.constructive.eo.avro.vulcan
Members list
Type members
Classlikes
vulcan → eo bridge: derive an AvroCodec from a vulcan.Codec (issue #73).
vulcan → eo bridge: derive an AvroCodec from a vulcan.Codec (issue #73).
Every typed entry point in eo-avro — codecPrism[A], AvroPrism.field / widenPath*, AvroTraversal, the AvroJson diagonals — is keyed on eo's own AvroCodec evidence. A codebase whose codecs are vulcan gets in through this bridge instead of hand-writing the same four-line adapter per use site:
import dev.constructive.eo.avro.vulcan.given // every vulcan.Codec[A] now serves as AvroCodec[A]
val p = codecPrism[ClickInfo].field(_.entities) // summons through the bridge
The bridge comes in two shapes. codec(schema) is total: the schema is in hand, there is nothing to resolve. codec(using) resolves the schema from the codec and can fail, so it returns Either[Exception, AvroCodec[A]]. The opt-in given below is the one site with no failure channel — a summon must produce a codec — so a schema that will not resolve fails eagerly, at the given site.
Error mapping — vulcan is Either-typed where eo is total or Throwable-typed:
- schema resolution:
Leftfromcodec(using); impossible forcodec(schema); eager at the given site for the given (see above). encodethrows on error (eo'sencode(a: A): Anyis total — an encode failure under a matching schema is a codec-definition bug, not a per-record condition).decodeerrors surface asLeftviaAvroError.throwable, matching AvroCodec's structured-failure convention.
vulcan rides on cats-eo-avro as an Optional dependency (the AvroJson / circe pattern): this sub-package's API surface names vulcan.Codec, so any caller already depends on vulcan directly — Optional keeps it off downstream classpaths, and avro-only users never load these classfiles.
Attributes
- Source
- AvroVulcan.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
AvroVulcan.type
The macro behind AvroVulcan.recordBuilder — walks A's case fields at EXPANSION and emits the WholeRecordBuilder.RecordShape the runtime assembly resolves against the codec's schema.
The macro behind AvroVulcan.recordBuilder — walks A's case fields at EXPANSION and emits the WholeRecordBuilder.RecordShape the runtime assembly resolves against the codec's schema.
The macro's whole job is classification: it never emits per-field code. Each case field becomes one WholeRecordBuilder.FieldShape arm —
Option[X](dealiased) → WholeRecordBuilder.OptionKind overX's classification;- Boolean / Int / Long / Float / Double / String → WholeRecordBuilder.DirectKind;
- a case class (Case-flagged class, not sealed, not a module, not an AnyVal) → WholeRecordBuilder.RecordKind with the sub-shape, or WholeRecordBuilder.SelfKind when the type is already an ancestor on the derivation path (recursive case classes terminate at compile time and resolve through the runtime level chain);
- everything else → WholeRecordBuilder.CodecKind holding the field type's own
vulcan.Codec, summoned HERE so the caller's scope answers for its leaves — a missing leaf codec is a compile error pointing at the exact field.
Because the emitted value is plain data, everything schema-dependent (slot resolution, arm validation) happens at builder construction in WholeRecordBuilder.derive — ordinary testable Scala, no staged code. The whole classification lives in builderImpl as local defs under ONE Quotes: TypeRepr / Symbol are path-dependent on the Quotes instance, so a (using Quotes)-taking helper called from inside a quote would type against a different path.
Attributes
- Source
- RecordBuilderMacro.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
RecordBuilderMacro.type
A compile-time-derived whole-record builder: A ⇒ GenericData.Record, leaf by leaf, with no codec composition on the hot path (issue #95).
A compile-time-derived whole-record builder: A ⇒ GenericData.Record, leaf by leaf, with no codec composition on the hot path (issue #95).
'''The use case.''' Building a fresh generic record from a typed value on a hot path — an ingest side, a replay, a batch flush. The full vulcan Codec[A].encode pays its composition once per FIELD, per LEVEL: a FreeApplicative.analyze, an Either + Chain.one per field, and a put(name, value) hash probe — and a nested sub-record field redoes all of it inside Codec[Sub].encode, which is what the filer measured as ~384–468 B/field on their real ClickInfo. A hand-built .put(pos, value) builder avoids all of it but costs one hand-maintained line per leaf — the exact complaint the filer opened the issue with.
'''What a derived builder is.''' AvroVulcan.recordBuilder walks A's case fields at COMPILE time (the WholeRecordBuilder.RecordShape IR) and emits one runtime assembly call; construction resolves every case field's schema slot by NAME (all-or-nothing, issue #105's doctrine) and validates every arm against the schema it will write into — so toRecord itself is nothing but positional puts: new GenericData.Record(schema), then per field either the value itself (a primitive leaf), a recursive sub-record build (a nested case class — the recursion the filer's positional prototype lacked, which is what made nested shapes pay vulcan's per-sub-record composition), null (a None), or the field type's own leaf codec (everything the fast arms don't cover). Schema-only fields (computed/derived columns the case class doesn't hold) stay at their in-record default.
'''Allocation is the gate, and it is hand-built-equal.''' Per record: the GenericData.Record values array plus one boxed value per primitive leaf — exactly what the hand-built builder allocates; the plans and slots are construction-time. ns/op stays within a small multiple of the hand-built form (one erasure-level dispatch per non-primitive leaf; the primitive bulk is a tight positional loop) and far below the codec composition it replaces — the benchmarks ClickRecordBench measures all of it side by side.
'''Construction is total.''' Every way assembly can fail — a case field no schema column answers for, two case fields claiming one column, an arm disagreeing with its schema field's shape, a non-record schema — comes back as the Exception half of the recordBuilder / derive result, naming the field and the record, BEFORE any record is built. toRecord itself is total for values matching A (a codec-leaf arm that fails encode still throws, per AvroCodec's total-encode convention — that is a codec-definition bug, not a construction condition).
'''The one behavioural difference from Codec[A].encode,''' stated because a wire-compat claim without it would be false precision: a schema-only (computed/derived) column. The codec fills it during encode; the builder leaves the slot at its in-record value (null on a fresh GenericData.Record) — identical to the hand-built .put builder the issue benchmarked, and round-trip-safe through the codec's own decode (which reconstructs A from the fields it knows).
Value parameters
- schema
-
the record schema the builder writes into — the codec's own schema object, so a record built by the builder and a record decoded by the codec share one identity.
Attributes
- Companion
- object
- Source
- WholeRecordBuilder.scala
- Supertypes
-
class Objecttrait Matchableclass Any
Attributes
- Companion
- class
- Source
- WholeRecordBuilder.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
WholeRecordBuilder.type
Givens
Givens
import dev.constructive.eo.avro.vulcan.given makes every in-scope vulcan.Codec[A] usable wherever eo demands AvroCodec[A] evidence. Opt-in by import — don't combine with kindlings-derived AvroCodec givens for the same A in one scope, or the summon turns ambiguous. A given must produce its value, so a schema that will not resolve fails HERE — prefer the explicit AvroVulcan.codec forms where an Either can be surfaced.
import dev.constructive.eo.avro.vulcan.given makes every in-scope vulcan.Codec[A] usable wherever eo demands AvroCodec[A] evidence. Opt-in by import — don't combine with kindlings-derived AvroCodec givens for the same A in one scope, or the summon turns ambiguous. A given must produce its value, so a schema that will not resolve fails HERE — prefer the explicit AvroVulcan.codec forms where an Either can be surfaced.
Attributes
- Source
- AvroVulcan.scala