Skip to content

Commit 939ae78

Browse files
committed
Use record/union APIs to create Magnolia codecs
1 parent 42fc46c commit 939ae78

File tree

3 files changed

+94
-381
lines changed

3 files changed

+94
-381
lines changed

modules/generic/src/main/scala-2/vulcan/generic/package.scala

Lines changed: 48 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import shapeless.{:+:, CNil, Coproduct, Inl, Inr, Lazy}
1616
import shapeless.ops.coproduct.{Inject, Selector}
1717
import vulcan.internal.converters.collection._
1818
import vulcan.internal.tags._
19+
import cats.data.Chain
20+
import cats.free.FreeApplicative
1921

2022
package object generic {
2123
implicit final val cnilCodec: Codec.Aux[Nothing, CNil] =
@@ -112,144 +114,52 @@ package object generic {
112114
implicit final class MagnoliaCodec private[generic] (
113115
private val codec: Codec.type
114116
) extends AnyVal {
115-
final def combine[A](caseClass: CaseClass[Codec, A]): Codec[A] = {
116-
val namespace =
117-
caseClass.annotations
118-
.collectFirst { case AvroNamespace(namespace) => namespace }
119-
.getOrElse(caseClass.typeName.owner)
120-
121-
val shortName =
122-
caseClass.annotations
123-
.collectFirst { case AvroName(namespace) => namespace }
124-
.getOrElse(caseClass.typeName.short)
125-
126-
val typeName =
127-
s"$namespace.$shortName"
128-
129-
val schema =
130-
if (caseClass.isValueClass) {
131-
caseClass.parameters.head.typeclass.schema
132-
} else {
133-
AvroError.catchNonFatal {
117+
final def combine[A](caseClass: CaseClass[Codec, A]): Codec[A] =
118+
if (caseClass.isValueClass) {
119+
val param = caseClass.parameters.head
120+
param.typeclass.imap(value => caseClass.rawConstruct(List(value)))(param.dereference)
121+
} else {
122+
123+
Codec
124+
.record[A](
125+
name = caseClass.annotations
126+
.collectFirst { case AvroName(namespace) => namespace }
127+
.getOrElse(caseClass.typeName.short),
128+
namespace = caseClass.annotations
129+
.collectFirst { case AvroNamespace(namespace) => namespace }
130+
.getOrElse(caseClass.typeName.owner),
131+
doc = caseClass.annotations.collectFirst {
132+
case AvroDoc(doc) => doc
133+
}
134+
) { (f: Codec.FieldBuilder[A]) =>
134135
val nullDefaultBase = caseClass.annotations
135136
.collectFirst { case AvroNullDefault(enabled) => enabled }
136137
.getOrElse(false)
137138

138-
val fields =
139-
caseClass.parameters.toList.traverse { param =>
140-
param.typeclass.schema.map { schema =>
141-
def nullDefaultField =
142-
param.annotations
143-
.collectFirst {
144-
case AvroNullDefault(nullDefault) => nullDefault
145-
}
146-
.getOrElse(nullDefaultBase)
147-
148-
new Schema.Field(
149-
param.label,
150-
schema,
151-
param.annotations.collectFirst {
152-
case AvroDoc(doc) => doc
153-
}.orNull,
154-
if (schema.isNullable && nullDefaultField) Schema.Field.NULL_DEFAULT_VALUE
155-
else null
156-
)
157-
}
139+
caseClass.parameters.toList
140+
.traverse[FreeApplicative[Codec.Field[A, *], *], Any] { param =>
141+
def nullDefaultField =
142+
param.annotations
143+
.collectFirst {
144+
case AvroNullDefault(nullDefault) => nullDefault
145+
}
146+
.getOrElse(nullDefaultBase)
147+
148+
implicit val codec = param.typeclass
149+
150+
f(
151+
name = param.label,
152+
access = param.dereference,
153+
doc = param.annotations.collectFirst {
154+
case AvroDoc(doc) => doc
155+
},
156+
default = (if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None)
157+
else None).asInstanceOf[Option[param.PType]] // TODO: remove cast
158+
).widen
158159
}
159-
160-
fields.map { fields =>
161-
Schema.createRecord(
162-
caseClass.annotations
163-
.collectFirst {
164-
case AvroName(name) => name
165-
}
166-
.getOrElse(
167-
caseClass.typeName.short
168-
),
169-
caseClass.annotations.collectFirst {
170-
case AvroDoc(doc) => doc
171-
}.orNull,
172-
namespace,
173-
false,
174-
fields.asJava
175-
)
176-
}
160+
.map(caseClass.rawConstruct(_))
177161
}
178-
}
179-
Codec
180-
.instance[Any, A](
181-
schema,
182-
if (caseClass.isValueClass) { a =>
183-
val param = caseClass.parameters.head
184-
param.typeclass.encode(param.dereference(a))
185-
} else
186-
(a: A) =>
187-
schema.flatMap { schema =>
188-
val fields =
189-
caseClass.parameters.toList.traverse { param =>
190-
param.typeclass
191-
.encode(param.dereference(a))
192-
.tupleLeft(param.label)
193-
}
194-
195-
fields.map { values =>
196-
val record = new GenericData.Record(schema)
197-
values.foreach {
198-
case (label, value) =>
199-
record.put(label, value)
200-
}
201-
202-
record
203-
}
204-
},
205-
if (caseClass.isValueClass) { (value, schema) =>
206-
caseClass.parameters.head.typeclass
207-
.decode(value, schema)
208-
.map(decoded => caseClass.rawConstruct(List(decoded)))
209-
} else
210-
(value, writerSchema) => {
211-
writerSchema.getType() match {
212-
case Schema.Type.RECORD =>
213-
value match {
214-
case record: IndexedRecord =>
215-
caseClass.parameters.toList
216-
.traverse {
217-
param =>
218-
val field = record.getSchema.getField(param.label)
219-
if (field != null) {
220-
val value = record.get(field.pos)
221-
param.typeclass.decode(value, field.schema())
222-
} else {
223-
schema.flatMap { readerSchema =>
224-
readerSchema.getFields.asScala
225-
.find(_.name == param.label)
226-
.filter(_.hasDefaultValue)
227-
.toRight(AvroError.decodeMissingRecordField(param.label))
228-
.flatMap(
229-
readerField => param.typeclass.decode(null, readerField.schema)
230-
)
231-
}
232-
}
233-
}
234-
.map(caseClass.rawConstruct)
235-
236-
case other =>
237-
Left(AvroError.decodeUnexpectedType(other, "IndexedRecord"))
238-
}
239-
240-
case schemaType =>
241-
Left {
242-
AvroError
243-
.decodeUnexpectedSchemaType(
244-
schemaType,
245-
Schema.Type.RECORD
246-
)
247-
}
248-
}
249-
}
250-
)
251-
.withTypeName(typeName)
252-
}
162+
}
253163

254164
/**
255165
* Returns a `Codec` instance for the specified type,
@@ -260,68 +170,15 @@ package object generic {
260170
macro Magnolia.gen[A]
261171

262172
final def dispatch[A](sealedTrait: SealedTrait[Codec, A]): Codec.Aux[Any, A] = {
263-
val typeName = sealedTrait.typeName.full
264-
Codec
265-
.instance[Any, A](
266-
AvroError.catchNonFatal {
267-
sealedTrait.subtypes.toList
268-
.traverse(_.typeclass.schema)
269-
.map(schemas => Schema.createUnion(schemas.asJava))
270-
},
271-
a =>
272-
sealedTrait.dispatch(a) { subtype =>
273-
subtype.typeclass.encode(subtype.cast(a))
274-
},
275-
(value, schema) => {
276-
val schemaTypes =
277-
schema.getType() match {
278-
case Schema.Type.UNION => schema.getTypes.asScala
279-
case _ => Seq(schema)
280-
}
281-
282-
value match {
283-
case container: GenericContainer =>
284-
val subtypeName =
285-
container.getSchema.getName
286173

287-
val subtypeUnionSchema =
288-
schemaTypes
289-
.find(_.getName == subtypeName)
290-
.toRight(AvroError.decodeMissingUnionSchema(subtypeName))
291-
292-
def subtypeMatching =
293-
sealedTrait.subtypes
294-
.find(_.typeclass.schema.exists(_.getName == subtypeName))
295-
.toRight(AvroError.decodeMissingUnionAlternative(subtypeName))
296-
297-
subtypeUnionSchema.flatMap { subtypeSchema =>
298-
subtypeMatching.flatMap { subtype =>
299-
subtype.typeclass.decode(container, subtypeSchema)
300-
}
301-
}
302-
303-
case other =>
304-
sealedTrait.subtypes.toList
305-
.collectFirstSome { subtype =>
306-
subtype.typeclass.schema
307-
.traverse { subtypeSchema =>
308-
val subtypeName = subtypeSchema.getName
309-
schemaTypes
310-
.find(_.getName == subtypeName)
311-
.flatMap { schema =>
312-
subtype.typeclass
313-
.decode(other, schema)
314-
.toOption
315-
}
316-
}
317-
}
318-
.getOrElse {
319-
Left(AvroError.decodeExhaustedAlternatives(other))
320-
}
174+
Codec
175+
.union[A](
176+
alt =>
177+
Chain.fromSeq(sealedTrait.subtypes).flatMap { subtype =>
178+
alt(subtype.typeclass, Prism.instance(subtype.cast.lift)(identity))
321179
}
322-
}
323180
)
324-
.withTypeName(typeName)
181+
.withTypeName(sealedTrait.typeName.full)
325182
}
326183

327184
final type Typeclass[A] = Codec[A]

0 commit comments

Comments
 (0)