dev.constructive.eo.avro.vulcan

Members list

Type members

Classlikes

object AvroVulcan

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: Left from codec(using); impossible for codec(schema); eager at the given site for the given (see above).
  • encode throws on error (eo's encode(a: A): Any is total — an encode failure under a matching schema is a codec-definition bug, not a per-record condition).
  • decode errors surface as Left via AvroError.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 Object
trait Matchable
class 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 —

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 Object
trait Matchable
class Any
Self type
final class WholeRecordBuilder[A]

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 Object
trait Matchable
class Any

Attributes

Companion
class
Source
WholeRecordBuilder.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Givens

Givens

given vulcanAvroCodec: [A] => Codec[A] => AvroCodec[A]

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