diff --git a/README.md b/README.md index 243bf44ee..2e33a70fe 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ _ZIO Schema_ is used by a growing number of ZIO libraries, including _ZIO Flow_, In order to use this library, we need to add the following lines in our `build.sbt` file: ```scala -libraryDependencies += "dev.zio" %% "zio-schema" % "0.4.11" -libraryDependencies += "dev.zio" %% "zio-schema-bson" % "0.4.11" -libraryDependencies += "dev.zio" %% "zio-schema-json" % "0.4.11" -libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "0.4.11" +libraryDependencies += "dev.zio" %% "zio-schema" % "0.4.13" +libraryDependencies += "dev.zio" %% "zio-schema-bson" % "0.4.13" +libraryDependencies += "dev.zio" %% "zio-schema-json" % "0.4.13" +libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "0.4.13" // Required for automatic generic derivation of schemas -libraryDependencies += "dev.zio" %% "zio-schema-derivation" % "0.4.11", +libraryDependencies += "dev.zio" %% "zio-schema-derivation" % "0.4.13", libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided" ``` diff --git a/build.sbt b/build.sbt index cb1db388f..cb32d750c 100644 --- a/build.sbt +++ b/build.sbt @@ -223,6 +223,7 @@ lazy val zioSchemaAvro = crossProject(JSPlatform, JVMPlatform) .in(file("zio-schema-avro")) .dependsOn(zioSchema, zioSchemaDerivation, tests % "test->test") .settings(stdSettings("zio-schema-avro")) + .settings(dottySettings) .settings(crossProjectSettings) .settings(buildInfoSettings("zio.schema.avro")) .settings( diff --git a/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroCodec.scala b/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroCodec.scala index 3500669b7..4359d0a21 100644 --- a/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroCodec.scala +++ b/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroCodec.scala @@ -1,959 +1,1000 @@ package zio.schema.codec -import java.nio.charset.StandardCharsets -import java.time.format.DateTimeFormatter -import java.time.{ Duration, Month, MonthDay, Period, Year, YearMonth } +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import java.util.UUID -import scala.annotation.StaticAnnotation import scala.collection.immutable.ListMap import scala.jdk.CollectionConverters._ -import scala.util.{ Right, Try } - -import org.apache.avro.{ LogicalTypes, Schema => SchemaAvro } - -import zio.Chunk -import zio.schema.CaseSet.Aux -import zio.schema.Schema.{ Record, _ } -import zio.schema._ -import zio.schema.codec.AvroAnnotations._ -import zio.schema.codec.AvroPropMarker._ -import zio.schema.meta.MetaSchema - -trait AvroCodec { - def encode(schema: Schema[_]): scala.util.Either[String, String] - - def decode(bytes: Chunk[Byte]): scala.util.Either[String, Schema[_]] +import scala.util.Try + +import org.apache.avro.generic.{ + GenericData, + GenericDatumReader, + GenericDatumWriter, + GenericRecord, + GenericRecordBuilder } +import org.apache.avro.io.{ DecoderFactory, EncoderFactory } +import org.apache.avro.util.Utf8 +import org.apache.avro.{ Conversions, LogicalTypes, Schema => SchemaAvro } -object AvroCodec extends AvroCodec { - - def encode(schema: Schema[_]): scala.util.Either[String, String] = - toAvroSchema(schema).map(_.toString) +import zio.schema.{ FieldSet, Schema, StandardType, TypeId } +import zio.stream.ZPipeline +import zio.{ Chunk, Unsafe, ZIO } - def encodeToApacheAvro(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = - toAvroSchema(schema) +object AvroCodec { - def decode(bytes: Chunk[Byte]): scala.util.Either[String, Schema[_]] = { - val avroSchemaParser = new SchemaAvro.Parser() - val avroSchema = Try { - avroSchemaParser.parse(new String(bytes.toArray, StandardCharsets.UTF_8)) - }.fold( - e => Left(e.getMessage), - s => Right(s) - ) - avroSchema.flatMap(toZioSchema) + trait ExtendedBinaryCodec[A] extends BinaryCodec[A] { + def encodeGenericRecord(value: A)(implicit schema: Schema[A]): GenericData.Record + def decodeGenericRecord(value: GenericRecord)(implicit schema: Schema[A]): Either[DecodeError, A] } - def decodeFromApacheAvro: SchemaAvro => scala.util.Either[String, Schema[_]] = toZioSchema - - private def toZioSchema(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = - for { - // make sure to parse logical types with throwing exceptions enabled, - // otherwise parsing errors on invalid logical types might be lost - _ <- Try { - LogicalTypes.fromSchema(avroSchema) - }.toEither.left.map(e => e.getMessage) - result <- avroSchema.getType match { - case SchemaAvro.Type.RECORD => - RecordType.fromAvroRecord(avroSchema) match { - case Some(RecordType.Period) => Right(Schema.primitive(StandardType.PeriodType)) - case Some(RecordType.YearMonth) => Right(Schema.primitive(StandardType.YearMonthType)) - case Some(RecordType.Tuple) => toZioTuple(avroSchema) - case Some(RecordType.MonthDay) => Right(Schema.primitive(StandardType.MonthDayType)) - case Some(RecordType.Duration) => Right(Schema.primitive(StandardType.DurationType)) - case None => toZioRecord(avroSchema) - } - case SchemaAvro.Type.ENUM => toZioStringEnum(avroSchema) - case SchemaAvro.Type.ARRAY => - toZioSchema(avroSchema.getElementType).map(Schema.list(_)) - case SchemaAvro.Type.MAP => - toZioSchema(avroSchema.getValueType).map(Schema.map(Schema.primitive(StandardType.StringType), _)) - case SchemaAvro.Type.UNION => - avroSchema match { - case OptionUnion(optionSchema) => toZioSchema(optionSchema).map(Schema.option(_)) - case EitherUnion(left, right) => - toZioSchema(left).flatMap(l => toZioSchema(right).map(r => Schema.either(l, r))) - case _ => toZioEnumeration(avroSchema) - } - case SchemaAvro.Type.FIXED => - val fixed = if (avroSchema.getLogicalType == null) { - Right(Schema.primitive(StandardType.BinaryType)) - } else if (avroSchema.getLogicalType.isInstanceOf[LogicalTypes.Decimal]) { - val size = avroSchema.getFixedSize - toZioDecimal(avroSchema, DecimalType.Fixed(size)) - } else { - // TODO: Java implementation of Apache Avro does not support logical type Duration yet: - // AVRO-2123 with PR https://github.com/apache/avro/pull/1263 - Left(s"Unsupported fixed logical type ${avroSchema.getLogicalType}") - } - fixed.map(_.addAllAnnotations(buildZioAnnotations(avroSchema))) - case SchemaAvro.Type.STRING => - StringType.fromAvroString(avroSchema) match { - case Some(stringType) => - val dateTimeFormatter = Formatter.fromAvroStringOrDefault(avroSchema, stringType) - dateTimeFormatter - .map(_.dateTimeFormatter) - .flatMap(_ => { - stringType match { - case StringType.ZoneId => Right(Schema.primitive(StandardType.ZoneIdType)) - case StringType.Instant => - Right( - Schema - .primitive(StandardType.InstantType) - .annotate(AvroAnnotations.formatToString) - ) - case StringType.LocalDate => - Right( - Schema - .primitive(StandardType.LocalDateType) - .annotate(AvroAnnotations.formatToString) - ) - case StringType.LocalTime => - Right( - Schema - .primitive(StandardType.LocalTimeType) - .annotate(AvroAnnotations.formatToString) - ) - case StringType.LocalDateTime => - Right( - Schema - .primitive(StandardType.LocalDateTimeType) - .annotate(AvroAnnotations.formatToString) - ) - case StringType.OffsetTime => - Right(Schema.primitive(StandardType.OffsetTimeType)) - case StringType.OffsetDateTime => - Right(Schema.primitive(StandardType.OffsetDateTimeType)) - case StringType.ZoneDateTime => - Right(Schema.primitive(StandardType.ZonedDateTimeType)) - } - }) - case None => - if (avroSchema.getLogicalType == null) { - Right(Schema.primitive(StandardType.StringType)) - } else if (avroSchema.getLogicalType.getName == LogicalTypes.uuid().getName) { - Right(Schema.primitive(StandardType.UUIDType)) - } else { - Left(s"Unsupported string logical type: ${avroSchema.getLogicalType.getName}") - } - } - case SchemaAvro.Type.BYTES => - if (avroSchema.getLogicalType == null) { - Right(Schema.primitive(StandardType.BinaryType)) - } else if (avroSchema.getLogicalType.isInstanceOf[LogicalTypes.Decimal]) { - toZioDecimal(avroSchema, DecimalType.Bytes) - } else { - Left(s"Unsupported bytes logical type ${avroSchema.getLogicalType.getName}") - } - case SchemaAvro.Type.INT => - IntType.fromAvroInt(avroSchema) match { - case Some(IntType.Char) => Right(Schema.primitive(StandardType.CharType)) - case Some(IntType.DayOfWeek) => Right(Schema.primitive(StandardType.DayOfWeekType)) - case Some(IntType.Year) => Right(Schema.primitive(StandardType.YearType)) - case Some(IntType.Short) => Right(Schema.primitive(StandardType.ShortType)) - case Some(IntType.Month) => Right(Schema.primitive(StandardType.MonthType)) - case Some(IntType.ZoneOffset) => Right(Schema.primitive(StandardType.ZoneOffsetType)) - case None => - if (avroSchema.getLogicalType == null) { - Right(Schema.primitive(StandardType.IntType)) - } else - avroSchema.getLogicalType match { - case _: LogicalTypes.TimeMillis => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.LocalTimeType) - ) - case _: LogicalTypes.Date => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.LocalDateType) - ) - case _ => Left(s"Unsupported int logical type ${avroSchema.getLogicalType.getName}") - } - } - case SchemaAvro.Type.LONG => - if (avroSchema.getLogicalType == null) { - Right(Schema.primitive(StandardType.LongType)) - } else - avroSchema.getLogicalType match { - case _: LogicalTypes.TimeMicros => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.LocalTimeType) - ) - case _: LogicalTypes.TimestampMillis => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.InstantType) - ) - case _: LogicalTypes.TimestampMicros => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.InstantType) - ) - case _: LogicalTypes.LocalTimestampMillis => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.LocalDateTimeType) - ) - case _: LogicalTypes.LocalTimestampMicros => - val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) - formatter.map( - _ => Schema.primitive(StandardType.LocalDateTimeType) - ) - case _ => Left(s"Unsupported long logical type ${avroSchema.getLogicalType.getName}") - } - case SchemaAvro.Type.FLOAT => Right(Schema.primitive(StandardType.FloatType)) - case SchemaAvro.Type.DOUBLE => Right(Schema.primitive(StandardType.DoubleType)) - case SchemaAvro.Type.BOOLEAN => Right(Schema.primitive(StandardType.BoolType)) - case SchemaAvro.Type.NULL => Right(Schema.primitive(StandardType.UnitType)) - case null => Left(s"Unsupported type ${avroSchema.getType}") - } - } yield result - - def toAvroBinary(schema: Schema[_]): SchemaAvro = - schema.annotations.collectFirst { - case AvroAnnotations.bytes(BytesType.Fixed(size, name, doc, space)) => - SchemaAvro.createFixed(name, doc, space, size) - }.getOrElse(SchemaAvro.create(SchemaAvro.Type.BYTES)) - - private[codec] lazy val monthDayStructure: Seq[Schema.Field[MonthDay, Int]] = Seq( - Schema.Field( - "month", - Schema.Primitive(StandardType.IntType), - get0 = _.getMonthValue, - set0 = (a, b: Int) => a.`with`(Month.of(b)) - ), - Schema.Field( - "day", - Schema.Primitive(StandardType.IntType), - get0 = _.getDayOfMonth, - set0 = (a, b: Int) => a.withDayOfMonth(b) - ) - ) - - private[codec] lazy val periodStructure: Seq[Schema.Field[Period, Int]] = Seq( - Schema - .Field("years", Schema.Primitive(StandardType.IntType), get0 = _.getYears, set0 = (a, b: Int) => a.withYears(b)), - Schema - .Field( - "months", - Schema.Primitive(StandardType.IntType), - get0 = _.getMonths, - set0 = (a, b: Int) => a.withMonths(b) - ), - Schema.Field("days", Schema.Primitive(StandardType.IntType), get0 = _.getDays, set0 = (a, b: Int) => a.withDays(b)) - ) - - private[codec] lazy val yearMonthStructure: Seq[Schema.Field[YearMonth, Int]] = Seq( - Schema.Field( - "year", - Schema.Primitive(StandardType.IntType), - get0 = _.getYear, - set0 = (a, b: Int) => a.`with`(Year.of(b)) - ), - Schema.Field( - "month", - Schema.Primitive(StandardType.IntType), - get0 = _.getMonthValue, - set0 = (a, b: Int) => a.`with`(Month.of(b)) - ) - ) - - private[codec] lazy val durationStructure: Seq[Schema.Field[Duration, _]] = Seq( - Schema.Field( - "seconds", - Schema.Primitive(StandardType.LongType), - get0 = _.getSeconds, - set0 = (a, b: Long) => a.plusSeconds(b) - ), - Schema.Field( - "nanos", - Schema.Primitive(StandardType.IntType), - get0 = _.getNano, - set0 = (a, b: Int) => a.plusNanos(b.toLong) - ) - ) - - private def toAvroSchema(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { - schema match { - case e: Enum[_] => toAvroEnum(e) - case record: Record[_] => toAvroRecord(record) - case map: Schema.Map[_, _] => toAvroMap(map) - case seq: Schema.Sequence[_, _, _] => toAvroSchema(seq.elementSchema).map(SchemaAvro.createArray) - case set: Schema.Set[_] => toAvroSchema(set.elementSchema).map(SchemaAvro.createArray) - case Transform(codec, _, _, _, _) => toAvroSchema(codec) - case Primitive(standardType, _) => - standardType match { - case StandardType.UnitType => Right(SchemaAvro.create(SchemaAvro.Type.NULL)) - case StandardType.StringType => Right(SchemaAvro.create(SchemaAvro.Type.STRING)) - case StandardType.BoolType => Right(SchemaAvro.create(SchemaAvro.Type.BOOLEAN)) - case StandardType.ShortType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Short))) - case StandardType.ByteType => Right(SchemaAvro.create(SchemaAvro.Type.INT)) - case StandardType.IntType => Right(SchemaAvro.create(SchemaAvro.Type.INT)) - case StandardType.LongType => Right(SchemaAvro.create(SchemaAvro.Type.LONG)) - case StandardType.FloatType => Right(SchemaAvro.create(SchemaAvro.Type.FLOAT)) - case StandardType.DoubleType => Right(SchemaAvro.create(SchemaAvro.Type.DOUBLE)) - case StandardType.BinaryType => Right(toAvroBinary(schema)) - case StandardType.CharType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Char))) - case StandardType.UUIDType => - Right(LogicalTypes.uuid().addToSchema(SchemaAvro.create(SchemaAvro.Type.STRING))) - case StandardType.BigDecimalType => toAvroDecimal(schema) - case StandardType.BigIntegerType => toAvroDecimal(schema) - case StandardType.DayOfWeekType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.DayOfWeek))) - case StandardType.MonthType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Month))) - case StandardType.YearType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Year))) - case StandardType.ZoneIdType => - Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(StringDiscriminator(StringType.ZoneId))) - case StandardType.ZoneOffsetType => - Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.ZoneOffset))) - case StandardType.MonthDayType => - //TODO 1 - //Right(SchemaAvro.create(monthDayStructure).addMarkerProp(RecordDiscriminator(RecordType.MonthDay))) - Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) - case StandardType.PeriodType => - //TODO 2 - //toAvroSchema(periodStructure).map(_.addMarkerProp(RecordDiscriminator(RecordType.Period))) - Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) - case StandardType.YearMonthType => - //TODO 3 - //toAvroSchema(yearMonthStructure).map(_.addMarkerProp(RecordDiscriminator(RecordType.YearMonth))) - Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) - case StandardType.DurationType => - // TODO: Java implementation of Apache Avro does not support logical type Duration yet: - // AVRO-2123 with PR https://github.com/apache/avro/pull/1263 - //TODO 4 - //val chronoUnitMarker = - //DurationChronoUnit.fromTemporalUnit(temporalUnit).getOrElse(DurationChronoUnit.default) - //toAvroSchema(durationStructure).map( - // _.addMarkerProp(RecordDiscriminator(RecordType.Duration)).addMarkerProp(chronoUnitMarker)) - Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) - - case StandardType.InstantType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.Instant)) - ) - case StandardType.LocalDateType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalDate)) - ) - case StandardType.LocalTimeType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalTime)) - ) - case StandardType.LocalDateTimeType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalDateTime)) - ) - case StandardType.OffsetTimeType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.OffsetTime)) - ) - case StandardType.OffsetDateTimeType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.OffsetDateTime)) - ) - case StandardType.ZonedDateTimeType => - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.ZoneDateTime)) - ) - } - case Optional(codec, _) => - for { - codecName <- getName(codec) - codecAvroSchema <- toAvroSchema(codec) - wrappedAvroSchema = codecAvroSchema match { - case schema: SchemaAvro if schema.getType == SchemaAvro.Type.NULL => - wrapAvro(schema, codecName, UnionWrapper) - case schema: SchemaAvro if schema.getType == SchemaAvro.Type.UNION => - wrapAvro(schema, codecName, UnionWrapper) - case schema => schema - } - } yield SchemaAvro.createUnion(SchemaAvro.create(SchemaAvro.Type.NULL), wrappedAvroSchema) - case Fail(message, _) => Left(message) - case tuple: Tuple2[_, _] => - toAvroSchema(tuple.toRecord).map( - _.addMarkerProp(RecordDiscriminator(RecordType.Tuple)) - ) - case e @ Schema.Either(left, right, _) => - val eitherUnion = for { - l <- toAvroSchema(left) - r <- toAvroSchema(right) - lname <- getName(left) - rname <- getName(right) - leftSchema = if (l.getType == SchemaAvro.Type.UNION) wrapAvro(l, lname, UnionWrapper) else l - rightSchema = if (r.getType == SchemaAvro.Type.UNION) wrapAvro(r, rname, UnionWrapper) else r - _ <- if (leftSchema.getFullName == rightSchema.getFullName) - Left(s"Left and right schemas of either must have different fullnames: ${leftSchema.getFullName}") - else Right(()) - } yield SchemaAvro.createUnion(leftSchema, rightSchema) - - // Unions in Avro can not hold additional properties, so we need to wrap the union in a record - for { - union <- eitherUnion - name <- getName(e) - } yield wrapAvro(union, name, EitherWrapper) - - case Lazy(schema0) => toAvroSchema(schema0()) - case Dynamic(_) => toAvroSchema(Schema[MetaSchema]) - } - } + implicit def schemaBasedBinaryCodec[A](implicit schema: Schema[A]): ExtendedBinaryCodec[A] = + new ExtendedBinaryCodec[A] { + + val avroSchema: SchemaAvro = + AvroSchemaCodec.encodeToApacheAvro(schema).getOrElse(throw new Exception("Avro schema could not be generated.")) + + override def encode(value: A): Chunk[Byte] = { + val baos = new ByteArrayOutputStream() + val datumWriter = new GenericDatumWriter[Any](avroSchema) + val datum = encodeValue(value, schema) + val serializer = EncoderFactory.get().directBinaryEncoder(baos, null) + datumWriter.write(datum, serializer) + val encoded = Chunk.fromArray(baos.toByteArray) + serializer.flush() + baos.close() + encoded + } - private def hasFormatToStringAnnotation(value: Chunk[Any]) = value.exists { - case AvroAnnotations.formatToString => true - case _ => false - } + override def streamEncoder: ZPipeline[Any, Nothing, A, Byte] = ZPipeline.mapChunks { chunk => + chunk.flatMap(encode) + } - private def getTimeprecisionType(value: Chunk[Any]): Option[TimePrecisionType] = value.collectFirst { - case AvroAnnotations.timeprecision(precision) => precision - } + override def decode(whole: Chunk[Byte]): Either[DecodeError, A] = { + val datumReader = new GenericDatumReader[Any](avroSchema) + val decoder = DecoderFactory.get().binaryDecoder(whole.toArray, null) + val decoded = datumReader.read(null, decoder) + decodeValue(decoded, schema) + } - private[codec] def toAvroInstant( - formatter: DateTimeFormatter, - annotations: Chunk[Any] - ): scala.util.Either[String, SchemaAvro] = - if (hasFormatToStringAnnotation(annotations)) { - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.Instant)) - .addMarkerProp(Formatter(formatter)) - ) - } else { - val baseSchema = SchemaAvro.create(SchemaAvro.Type.LONG) - getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { - case TimePrecisionType.Millis => - Right(LogicalTypes.timestampMillis().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) - case TimePrecisionType.Micros => - Right(LogicalTypes.timestampMicros().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) + override def streamDecoder: ZPipeline[Any, DecodeError, Byte, A] = ZPipeline.mapChunksZIO { chunk => + ZIO.fromEither( + decode(chunk).map(Chunk(_)) + ) } - } - private[codec] def toAvroLocalDate( - formatter: DateTimeFormatter, - annotations: Chunk[Any] - ): scala.util.Either[String, SchemaAvro] = - if (hasFormatToStringAnnotation(annotations)) { - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalDate)) - .addMarkerProp(Formatter(formatter)) - ) - } else { - Right(LogicalTypes.date().addToSchema(SchemaAvro.create(SchemaAvro.Type.INT)).addMarkerProp(Formatter(formatter))) - } + override def encodeGenericRecord(value: A)(implicit schema: Schema[A]): GenericData.Record = + encodeValue(value, schema).asInstanceOf[GenericData.Record] - private[codec] def toAvroLocalTime( - formatter: DateTimeFormatter, - annotations: Chunk[Any] - ): scala.util.Either[String, SchemaAvro] = - if (hasFormatToStringAnnotation(annotations)) { - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalTime)) - .addMarkerProp(Formatter(formatter)) - ) - } else { - getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { - case TimePrecisionType.Millis => - Right( - LogicalTypes - .timeMillis() - .addToSchema(SchemaAvro.create(SchemaAvro.Type.INT)) - .addMarkerProp(Formatter(formatter)) - ) - case TimePrecisionType.Micros => - Right( - LogicalTypes - .timeMicros() - .addToSchema(SchemaAvro.create(SchemaAvro.Type.LONG)) - .addMarkerProp(Formatter(formatter)) - ) - } + override def decodeGenericRecord(value: GenericRecord)(implicit schema: Schema[A]): Either[DecodeError, A] = + decodeValue(value, schema) } - private[codec] def toAvroLocalDateTime( - formatter: DateTimeFormatter, - annotations: Chunk[Any] - ): scala.util.Either[String, SchemaAvro] = - if (hasFormatToStringAnnotation(annotations)) { - Right( - SchemaAvro - .create(SchemaAvro.Type.STRING) - .addMarkerProp(StringDiscriminator(StringType.LocalDateTime)) - .addMarkerProp(Formatter(formatter)) + private def decodeValue[A](raw: Any, schema: Schema[A]): Either[DecodeError, A] = schema match { + case Schema.Enum1(_, c1, _) => decodeEnum(raw, c1).map(_.asInstanceOf[A]) + case Schema.Enum2(_, c1, c2, _) => decodeEnum(raw, c1, c2).map(_.asInstanceOf[A]) + case Schema.Enum3(_, c1, c2, c3, _) => decodeEnum(raw, c1, c2, c3).map(_.asInstanceOf[A]) + case Schema.Enum4(_, c1, c2, c3, c4, _) => decodeEnum(raw, c1, c2, c3, c4).map(_.asInstanceOf[A]) + case Schema.Enum5(_, c1, c2, c3, c4, c5, _) => decodeEnum(raw, c1, c2, c3, c4, c5).map(_.asInstanceOf[A]) + case Schema.Enum6(_, c1, c2, c3, c4, c5, c6, _) => decodeEnum(raw, c1, c2, c3, c4, c5, c6).map(_.asInstanceOf[A]) + case Schema.Enum7(_, c1, c2, c3, c4, c5, c6, c7, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7).map(_.asInstanceOf[A]) + case Schema.Enum8(_, c1, c2, c3, c4, c5, c6, c7, c8, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8).map(_.asInstanceOf[A]) + case Schema.Enum9(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9).map(_.asInstanceOf[A]) + case Schema.Enum10(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10).map(_.asInstanceOf[A]) + case Schema.Enum11(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11).map(_.asInstanceOf[A]) + case Schema.Enum12(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12).map(_.asInstanceOf[A]) + case Schema.Enum13(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13).map(_.asInstanceOf[A]) + case Schema.Enum14(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14).map(_.asInstanceOf[A]) + case Schema.Enum15(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15).map(_.asInstanceOf[A]) + case Schema.Enum16(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16).map(_.asInstanceOf[A]) + case Schema.Enum17(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17).map(_.asInstanceOf[A]) + case Schema.Enum18(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18).map( + _.asInstanceOf[A] ) - } else { - val baseSchema = SchemaAvro.create(SchemaAvro.Type.LONG) - getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { - case TimePrecisionType.Millis => - Right(LogicalTypes.localTimestampMillis().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) - case TimePrecisionType.Micros => - Right(LogicalTypes.localTimestampMicros().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) - } - } - - def hasAvroEnumAnnotation(annotations: Chunk[Any]): Boolean = annotations.exists { - case AvroAnnotations.avroEnum => true - case _ => false - } - - def wrapAvro(schemaAvro: SchemaAvro, name: String, marker: AvroPropMarker): SchemaAvro = { - val field = new SchemaAvro.Field("value", schemaAvro) - val fields = new java.util.ArrayList[SchemaAvro.Field]() - fields.add(field) - val prefixedName = s"${AvroPropMarker.wrapperNamePrefix}_$name" - SchemaAvro - .createRecord(prefixedName, null, AvroPropMarker.wrapperNamespace, false, fields) - .addMarkerProp(marker) + case Schema.Enum19(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19).map( + _.asInstanceOf[A] + ) + case Schema + .Enum20(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, _) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20).map( + _.asInstanceOf[A] + ) + case Schema.Enum21( + _, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + _ + ) => + decodeEnum(raw, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, c21) + .map(_.asInstanceOf[A]) + case Schema.Enum22( + _, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22, + _ + ) => + decodeEnum( + raw, + c1, + c2, + c3, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22 + ).map(_.asInstanceOf[A]) + case s0 @ Schema.CaseClass0(_, _, _) => + decodePrimitiveValues(raw, StandardType.UnitType).map(_ => s0.defaultConstruct()) + case s1 @ Schema.CaseClass1(_, _, _, _) => decodeCaseClass1(raw, s1) + case record: Schema.Record[_] => decodeRecord(raw, record).map(_.asInstanceOf[A]) + case Schema.Sequence(element, f, _, _, _) => + decodeSequence(raw, element.asInstanceOf[Schema[Any]]).map(f.asInstanceOf[Chunk[Any] => A]) + case Schema.Set(element, _) => decodeSequence(raw, element.asInstanceOf[Schema[Any]]).map(_.toSet.asInstanceOf[A]) + case mapSchema: Schema.Map[_, _] => + decodeMap(raw, mapSchema.asInstanceOf[Schema.Map[Any, Any]]).map(_.asInstanceOf[A]) + case Schema.Transform(schema, f, _, _, _) => + decodeValue(raw, schema).flatMap( + a => f(a).left.map(msg => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), msg)) + ) + case Schema.Primitive(standardType, _) => decodePrimitiveValues(raw, standardType) + case Schema.Optional(schema, _) => decodeOptionalValue(raw, schema) + case Schema.Fail(message, _) => Left(DecodeError.MalformedFieldWithPath(Chunk.empty, message)) + case Schema.Tuple2(left, right, _) => decodeTuple2(raw, left, right).map(_.asInstanceOf[A]) + case Schema.Either(left, right, _) => decodeEitherValue(raw, left, right) + case lzy @ Schema.Lazy(_) => decodeValue(raw, lzy.schema) + case unknown => Left(DecodeError.MalformedFieldWithPath(Chunk.empty, s"Unknown schema: $unknown")) } - private[codec] def toAvroEnum(enu: Enum[_]): scala.util.Either[String, SchemaAvro] = { - val avroEnumAnnotationExists = hasAvroEnumAnnotation(enu.annotations) - val isAvroEnumEquivalent = enu.cases.map(_.schema).forall { - case (Transform(Primitive(standardType, _), _, _, _, _)) - if standardType == StandardType.UnitType && avroEnumAnnotationExists => - true - case (Primitive(standardType, _)) if standardType == StandardType.StringType => true - case (CaseClass0(_, _, _)) if avroEnumAnnotationExists => true - case _ => false + private def decodeCaseClass1[A, Z](raw: Any, schema: Schema.CaseClass1[A, Z]) = + decodeValue(raw, schema.field.schema).map(schema.defaultConstruct) + + private def decodeEnum[Z](raw: Any, cases: Schema.Case[Z, _]*): Either[DecodeError, Any] = + raw match { + case enums: GenericData.EnumSymbol => + decodeGenericEnum(enums.toString, None, cases: _*) + case gr: GenericData.Record => + val enumCaseName = gr.getSchema.getFullName + if (gr.hasField("value")) { + val enumCaseValue = gr.get("value") + decodeGenericEnum[Z](enumCaseName, Some(enumCaseValue), cases: _*) + } else { + decodeGenericEnum[Z](enumCaseName, None, cases: _*) + } + case _ => Left(DecodeError.MalformedFieldWithPath(Chunk.single("Error"), s"Unknown enum: $raw")) } - if (isAvroEnumEquivalent) { - for { - name <- getName(enu) - doc = getDoc(enu.annotations).orNull - namespaceOption <- getNamespace(enu.annotations) - symbols = enu.cases.map { - case caseValue => getNameOption(caseValue.annotations).getOrElse(caseValue.id) - }.toList - result = SchemaAvro.createEnum(name, doc, namespaceOption.orNull, symbols.asJava) - } yield result - } else { - val cases = enu.cases.map(c => (c.id, (c.schema, c.annotations))).map { - case (symbol, (Transform(Primitive(standardType, _), _, _, _, _), annotations)) - if standardType == StandardType.UnitType => - val name = getNameOption(annotations).getOrElse(symbol) - Right(SchemaAvro.createRecord(name, null, null, false, new java.util.ArrayList[SchemaAvro.Field])) - case (symbol, (CaseClass0(_, _, _), annotations)) => - val name = getNameOption(annotations).getOrElse(symbol) - Right(SchemaAvro.createRecord(name, null, null, false, new java.util.ArrayList[SchemaAvro.Field])) - case (symbol, (schema, annotations)) => - val name = getNameOption(annotations).getOrElse(symbol) - val schemaWithName = addNameAnnotationIfMissing(schema, name) - toAvroSchema(schemaWithName).map { - case schema: SchemaAvro if schema.getType == SchemaAvro.Type.UNION => - wrapAvro(schema, name, UnionWrapper) // handle nested unions - case schema => schema + + private def decodeGenericEnum[Z]( + enumCaseName: String, + enumCaseValue: Option[AnyRef], + cases: Schema.Case[Z, _]* + ): Either[DecodeError, Any] = + cases + .find(_.id == enumCaseName) + .map(s => decodeValue(enumCaseValue.getOrElse(s), s.schema)) + .toRight(DecodeError.MalformedFieldWithPath(Chunk.single("Error"), s"Unknown enum value: $enumCaseName")) + .flatMap(identity) + + private def decodeRecord[A](value: A, schema: Schema.Record[_]) = { + val record = value.asInstanceOf[GenericRecord] + val fields = schema.fields + val decodedFields: Either[DecodeError, ListMap[String, Any]] = + fields.foldLeft[Either[DecodeError, ListMap[String, Any]]](Right(ListMap.empty)) { + case (Right(acc), field) => + val fieldName = field.name + val fieldValue = record.get(fieldName) + val decodedField = decodeValue(fieldValue, field.schema).map { value => + acc + (fieldName -> value) } + decodedField + case (Left(error), _) => Left(error) } - cases.toList.map(_.merge).partition { - case _: String => true - case _ => false - } match { - case (Nil, right: List[org.apache.avro.Schema @unchecked]) => Right(SchemaAvro.createUnion(right.asJava)) - case (left, _) => Left(left.mkString("\n")) + implicit val unsafe: Unsafe = Unsafe.unsafe + decodedFields.flatMap { fields => + schema.construct(Chunk.fromIterable(fields.values)).left.map { error => + DecodeError.MalformedFieldWithPath(Chunk.single("Error"), error) } } } - private def extractAvroFields(record: Record[_]): List[org.apache.avro.Schema.Field] = - record.fields.map(toAvroRecordField).toList.map(_.merge).partition { - case _: String => true - case _ => false - } match { - case (Nil, right: List[org.apache.avro.Schema.Field @unchecked]) => right - case _ => null + private def decodePrimitiveValues[A](value: Any, standardTypeSchema: StandardType[A]): Either[DecodeError, A] = + standardTypeSchema match { + case StandardType.UnitType => + Try(()).toEither.left.map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.StringType => + Try(value.asInstanceOf[Utf8].toString).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.BoolType => + Try(value.asInstanceOf[Boolean]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.ByteType => + Try(value.asInstanceOf[Integer]).toEither + .map(_.toByte) + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.ShortType => + Try(value.asInstanceOf[Integer]).toEither + .map(_.toShort) + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.IntType => + Try(value.asInstanceOf[Integer]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + .map(_.asInstanceOf[A]) + case StandardType.LongType => + Try(value.asInstanceOf[Long]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.FloatType => + Try(value.asInstanceOf[Float]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.DoubleType => + Try(value.asInstanceOf[Double]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.BinaryType => + Try(value.asInstanceOf[ByteBuffer].array().asInstanceOf[A]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.CharType => + Try(value.asInstanceOf[Integer]).toEither + .map(_.toChar) + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.UUIDType => + Try(UUID.fromString(value.asInstanceOf[Utf8].toString).asInstanceOf[A]).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.single("Error"), e.getMessage)) + case StandardType.BigDecimalType => + val converter = new Conversions.DecimalConversion() + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Primitive(StandardType.BigDecimalType, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for BigDecimal.")) + Try(converter.fromBytes(value.asInstanceOf[ByteBuffer], schema, LogicalTypes.decimal(48, 24))).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.BigIntegerType => + val converter = new Conversions.DecimalConversion() + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Primitive(StandardType.BigIntegerType, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for BigInteger.")) + Try(converter.fromBytes(value.asInstanceOf[ByteBuffer], schema, LogicalTypes.decimal(48, 24))).toEither.left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + .map(_.toBigInteger) + case StandardType.DayOfWeekType => + Try(value.asInstanceOf[Integer]) + .map(java.time.DayOfWeek.of(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.MonthType => + Try(value.asInstanceOf[Integer]) + .map(java.time.Month.of(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.MonthDayType => + Try(value.asInstanceOf[Utf8]) + .map(raw => java.time.MonthDay.parse(raw.toString)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.PeriodType => + Try(value.asInstanceOf[Utf8]) + .map(raw => java.time.Period.parse(raw.toString)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.YearType => + Try(value.asInstanceOf[Integer]) + .map(java.time.Year.of(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.YearMonthType => + Try(value.asInstanceOf[Utf8]) + .map(raw => java.time.YearMonth.parse(raw.toString)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.ZoneIdType => + Try(value.asInstanceOf[Utf8]) + .map(raw => java.time.ZoneId.of(raw.toString)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.ZoneOffsetType => + Try(value.asInstanceOf[Integer]) + .map(java.time.ZoneOffset.ofTotalSeconds(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.DurationType => + Try(value.asInstanceOf[Utf8]) + .map(raw => java.time.Duration.parse(raw.toString)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.InstantType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.Instant.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.LocalDateType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.LocalDate.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.LocalTimeType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.LocalTime.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.LocalDateTimeType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.LocalDateTime.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.OffsetTimeType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.OffsetTime.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.OffsetDateTimeType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.OffsetDateTime.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) + case StandardType.ZonedDateTimeType => + Try(value.asInstanceOf[Utf8]) + .map(java.time.ZonedDateTime.parse(_)) + .toEither + .left + .map(e => DecodeError.MalformedFieldWithPath(Chunk.empty, e.getMessage)) } - private[codec] def toAvroRecord(record: Record[_]): scala.util.Either[String, SchemaAvro] = - for { - name <- getName(record) - namespaceOption <- getNamespace(record.annotations) - result <- Right( - SchemaAvro.createRecord( - name, - getDoc(record.annotations).orNull, - namespaceOption.orNull, - isErrorRecord(record), - extractAvroFields(record).asJava - ) - ) - } yield result - - private[codec] def toAvroMap(map: Map[_, _]): scala.util.Either[String, SchemaAvro] = - map.keySchema match { - case p: Schema.Primitive[_] if p.standardType == StandardType.StringType => - toAvroSchema(map.valueSchema).map(SchemaAvro.createMap) - case _ => - val tupleSchema = Schema - .Tuple2(map.keySchema, map.valueSchema) - .annotate(AvroAnnotations.name("Tuple")) - .annotate(AvroAnnotations.namespace("scala")) - toAvroSchema(tupleSchema).map(SchemaAvro.createArray) + private def decodeMap(value: Any, schema: Schema.Map[Any, Any]) = { + val map = value.asInstanceOf[java.util.Map[Any, Any]] + val result: List[(Either[DecodeError, Any], Either[DecodeError, Any])] = map.asScala.toList.map { + case (k, v) => (decodeValue(k, schema.keySchema), decodeValue(v, schema.valueSchema)) } - - private[codec] def toAvroDecimal(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { - val scale = schema.annotations.collectFirst { case AvroAnnotations.scale(s) => s } - .getOrElse(AvroAnnotations.scale().scale) - val precision = schema match { - case Primitive(StandardType.BigDecimalType, _) => - schema.annotations.collectFirst { case AvroAnnotations.precision(p) => p } - .getOrElse(Math.max(scale, AvroAnnotations.precision().precision)) - case _ => scale + val traversed: Either[List[DecodeError], List[(Any, Any)]] = result.partition { + case (k, v) => k.isLeft || v.isLeft + } match { + case (Nil, decoded) => Right(for ((Right(k), Right(v)) <- decoded) yield (k, v)) + case (errors, _) => Left(for ((Left(s), _) <- errors) yield s) } - val baseAvroType = schema.annotations.collectFirst { case AvroAnnotations.decimal(decimalType) => decimalType } - .getOrElse(DecimalType.default) match { - case DecimalType.Fixed(size) => - for { - namespaceOption <- getNamespace(schema.annotations) - name = getNameOption(schema.annotations).getOrElse(s"Decimal_${precision}_$scale") - doc = getDoc(schema.annotations).orNull - result = SchemaAvro.createFixed(name, doc, namespaceOption.orNull, size) - } yield result - case DecimalType.Bytes => Right(SchemaAvro.create(SchemaAvro.Type.BYTES)) + val combined: Either[DecodeError, List[(Any, Any)]] = traversed.left.map { errors => + errors.foldLeft[DecodeError](DecodeError.MalformedFieldWithPath(Chunk.empty, "Map decoding failed."))( + (acc, error) => acc.and(DecodeError.MalformedFieldWithPath(Chunk.empty, s"${error.message}")) + ) } - baseAvroType.map( - LogicalTypes - .decimal(precision, scale) - .addToSchema(_) - ) - } - private[codec] def toErrorMessage(err: Throwable, at: AnyRef) = - s"Error mapping to Apache Avro schema: $err at ${at.toString}" - - private[codec] def toAvroRecordField[Z](value: Field[Z, _]): scala.util.Either[String, SchemaAvro.Field] = - toAvroSchema(value.schema).map( - schema => - new SchemaAvro.Field( - getNameOption(value.annotations).getOrElse(value.name), - schema, - getDoc(value.annotations).orNull, - getDefault(value.annotations).orNull, - getFieldOrder(value.annotations).map(_.toAvroOrder).getOrElse(FieldOrderType.default.toAvroOrder) - ) - ) + combined.map(_.toMap) - private[codec] def getFieldOrder(annotations: Chunk[Any]): Option[FieldOrderType] = - annotations.collectFirst { case AvroAnnotations.fieldOrder(fieldOrderType) => fieldOrderType } + } + private def decodeSequence[A](a: A, schema: Schema[A]) = { + val array = a.asInstanceOf[GenericData.Array[Any]] + val result = array.asScala.toList.map(decodeValue(_, schema)) + val traversed: Either[List[DecodeError], List[A]] = result.partition(_.isLeft) match { + case (Nil, decoded) => Right(for (Right(i) <- decoded) yield i) + case (errors, _) => Left(for (Left(s) <- errors) yield s) + } + val combined: Either[DecodeError, List[A]] = traversed.left.map { errors => + errors.foldLeft[DecodeError](DecodeError.MalformedFieldWithPath(Chunk.empty, "Sequence decoding failed."))( + (acc, error) => acc.and(DecodeError.MalformedFieldWithPath(Chunk.empty, s"${error.message}")) + ) + } - private[codec] def getName(schema: Schema[_]): scala.util.Either[String, String] = { - val validNameRegex = raw"[A-Za-z_][A-Za-z0-9_]*".r + combined.map(Chunk.fromIterable(_)) + } - schema.annotations.collectFirst { case AvroAnnotations.name(name) => name } match { - case Some(s) => - s match { - case validNameRegex() => Right(s) - case _ => - Left(s"Invalid Avro name: $s") - } - case None => - schema match { - case r: Record[_] => Right(r.id.name) - case e: Enum[_] => Right(e.id.name) - case _ => Right(s"hashed_${schema.ast.toString.hashCode().toString.replaceFirst("-", "n")}") - // TODO: better way to generate a (maybe stable) name? - } - } + private def decodeTuple2[A, B](value: Any, schemaLeft: Schema[A], schemaRight: Schema[B]) = { + val record = value.asInstanceOf[GenericRecord] + val result1 = decodeValue(record.get("_1"), schemaLeft) + val result2 = decodeValue(record.get("_2"), schemaRight) + result1.flatMap(a => result2.map(b => (a, b))) + } + private def decodeEitherValue[A, B](value: Any, schemaLeft: Schema[A], schemaRight: Schema[B]) = { + val record = value.asInstanceOf[GenericRecord] + val result = decodeValue(record.get("value"), schemaLeft) + if (result.isRight) result.map(Left(_)) + else decodeValue(record.get("value"), schemaRight).map(Right(_)) } - private[codec] def getNameOption(annotations: Chunk[Any]): Option[String] = - annotations.collectFirst { case AvroAnnotations.name(name) => name } + private def decodeOptionalValue[A](value: Any, schema: Schema[A]) = + if (value == null) Right(None) + else decodeValue(value, schema).map(Some(_)) + + private def encodeValue[A](a: A, schema: Schema[A]): Any = schema match { + case Schema.Enum1(_, c1, _) => encodeEnum(schema, a, c1) + case Schema.Enum2(_, c1, c2, _) => encodeEnum(schema, a, c1, c2) + case Schema.Enum3(_, c1, c2, c3, _) => encodeEnum(schema, a, c1, c2, c3) + case Schema.Enum4(_, c1, c2, c3, c4, _) => encodeEnum(schema, a, c1, c2, c3, c4) + case Schema.Enum5(_, c1, c2, c3, c4, c5, _) => encodeEnum(schema, a, c1, c2, c3, c4, c5) + case Schema.Enum6(_, c1, c2, c3, c4, c5, c6, _) => encodeEnum(schema, a, c1, c2, c3, c4, c5, c6) + case Schema.Enum7(_, c1, c2, c3, c4, c5, c6, c7, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7) + case Schema.Enum8(_, c1, c2, c3, c4, c5, c6, c7, c8, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8) + case Schema.Enum9(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9) + case Schema.Enum10(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) + case Schema.Enum11(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) + case Schema.Enum12(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) + case Schema.Enum13(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13) + case Schema.Enum14(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14) + case Schema.Enum15(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15) + case Schema.Enum16(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16) + case Schema.Enum17(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17) + case Schema.Enum18(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18) + case Schema.Enum19(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19) + case Schema + .Enum20(_, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20, _) => + encodeEnum(schema, a, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16, c17, c18, c19, c20) + case Schema.Enum21( + _, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + _ + ) => + encodeEnum( + schema, + a, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21 + ) + case Schema.Enum22( + _, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22, + _ + ) => + encodeEnum( + schema, + a, + c1, + c2, + c3, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + c9, + c10, + c11, + c12, + c13, + c14, + c15, + c16, + c17, + c18, + c19, + c20, + c21, + c22 + ) + case Schema.GenericRecord(typeId, structure, _) => encodeGenericRecord(a, typeId, structure) + case Schema.Primitive(standardType, _) => encodePrimitive(a, standardType) + case Schema.Sequence(element, _, g, _, _) => encodeSequence(element, g(a)) + case Schema.Set(element, _) => encodeSet(element, a) + case mapSchema: Schema.Map[_, _] => + encodeMap(mapSchema.asInstanceOf[Schema.Map[Any, Any]], a.asInstanceOf[scala.collection.immutable.Map[Any, Any]]) + case Schema.Transform(schema, _, g, _, _) => + g(a).map(encodeValue(_, schema)).getOrElse(throw new Exception("Transform failed.")) + case Schema.Optional(schema, _) => encodeOption(schema, a) + case Schema.Tuple2(left, right, _) => + encodeTuple2(left.asInstanceOf[Schema[Any]], right.asInstanceOf[Schema[Any]], a) + case Schema.Either(left, right, _) => encodeEither(left, right, a) + case Schema.Lazy(schema0) => encodeValue(a, schema0()) + case Schema.CaseClass0(_, _, _) => + encodeCaseClass(schema, a, Seq.empty: _*) //encodePrimitive((), StandardType.UnitType) + case Schema.CaseClass1(_, f, _, _) => encodeCaseClass(schema, a, f) + case Schema.CaseClass2(_, f0, f1, _, _) => encodeCaseClass(schema, a, f0, f1) + case Schema.CaseClass3(_, f0, f1, f2, _, _) => encodeCaseClass(schema, a, f0, f1, f2) + case Schema.CaseClass4(_, f0, f1, f2, f3, _, _) => encodeCaseClass(schema, a, f0, f1, f2, f3) + case Schema.CaseClass5(_, f0, f1, f2, f3, f4, _, _) => encodeCaseClass(schema, a, f0, f1, f2, f3, f4) + case Schema.CaseClass6(_, f0, f1, f2, f3, f4, f5, _, _) => encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5) + case Schema.CaseClass7(_, f0, f1, f2, f3, f4, f5, f6, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6) + case Schema.CaseClass8(_, f0, f1, f2, f3, f4, f5, f6, f7, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7) + case Schema.CaseClass9(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8) + case Schema.CaseClass10(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9) + case Schema.CaseClass11(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10) + case Schema.CaseClass12(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11) + case Schema.CaseClass13(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12) + case Schema.CaseClass14(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13) + case Schema.CaseClass15(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14) + case Schema.CaseClass16(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15) + case Schema.CaseClass17(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16) + case Schema.CaseClass18(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17) + case Schema + .CaseClass19(_, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, _, _) => + encodeCaseClass(schema, a, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18) + case Schema.CaseClass20( + _, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + _ + ) => + encodeCaseClass( + schema, + a, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19 + ) + case Schema.CaseClass21( + _, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + tail + ) => + encodeCaseClass( + schema, + a, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + tail._1 + ) + case Schema.CaseClass22( + _, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + tail + ) => + encodeCaseClass( + schema, + a, + f0, + f1, + f2, + f3, + f4, + f5, + f6, + f7, + f8, + f9, + f10, + f11, + f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + tail._1, + tail._2 + ) - private[codec] def getDoc(annotations: Chunk[Any]): Option[String] = - annotations.collectFirst { case AvroAnnotations.doc(doc) => doc } + case _ => throw new Exception(s"Unsupported schema $schema") - private[codec] def getDefault(annotations: Chunk[Any]): Option[java.lang.Object] = - annotations.collectFirst { case AvroAnnotations.default(javaDefaultObject) => javaDefaultObject } + } - private[codec] def getNamespace(annotations: Chunk[Any]): scala.util.Either[String, Option[String]] = { - val validNamespaceRegex = raw"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*".r + private def encodePrimitive[A](a: A, standardType: StandardType[A]): Any = + standardType match { + case StandardType.UnitType => null + case StandardType.StringType => new Utf8(a.asInstanceOf[String]) + case StandardType.BoolType => java.lang.Boolean.valueOf(a.asInstanceOf[Boolean]) + case StandardType.ByteType => java.lang.Byte.valueOf(a.asInstanceOf[Byte]) + case StandardType.ShortType => java.lang.Short.valueOf(a.asInstanceOf[Short]) + case StandardType.IntType => java.lang.Integer.valueOf(a.asInstanceOf[Int]) + case StandardType.LongType => java.lang.Long.valueOf(a.asInstanceOf[Long]) + case StandardType.FloatType => java.lang.Float.valueOf(a.asInstanceOf[Float]) + case StandardType.DoubleType => java.lang.Double.valueOf(a.asInstanceOf[Double]) + case StandardType.BinaryType => ByteBuffer.wrap(a.asInstanceOf[Chunk[Byte]].toArray) + case StandardType.CharType => java.lang.Short.valueOf(a.asInstanceOf[Char].toShort) + case StandardType.UUIDType => new Utf8(a.asInstanceOf[UUID].toString) + case StandardType.BigDecimalType => + val converter = new Conversions.DecimalConversion() + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Primitive(StandardType.BigDecimalType, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for BigDecimal.")) + converter.toBytes(a.asInstanceOf[java.math.BigDecimal], schema, LogicalTypes.decimal(48, 24)) + + case StandardType.BigIntegerType => + val converter = new Conversions.DecimalConversion() + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Primitive(StandardType.BigIntegerType, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for BigInteger.")) + val transformed = BigDecimal(a.asInstanceOf[java.math.BigInteger]) + converter.toBytes(transformed.underlying(), schema, LogicalTypes.decimal(48, 24)) + + case StandardType.DayOfWeekType => + a.asInstanceOf[java.time.DayOfWeek].getValue + + case StandardType.MonthType => + a.asInstanceOf[java.time.Month].getValue + case StandardType.MonthDayType => + val monthDay = a.asInstanceOf[java.time.MonthDay] + monthDay.toString + case StandardType.PeriodType => + val period = a.asInstanceOf[java.time.Period] + period.toString + case StandardType.YearType => + a.asInstanceOf[java.time.Year].getValue + case StandardType.YearMonthType => + val yearMonth = a.asInstanceOf[java.time.YearMonth] + yearMonth.toString + case StandardType.ZoneIdType => + a.asInstanceOf[java.time.ZoneId].toString + case StandardType.ZoneOffsetType => + a.asInstanceOf[java.time.ZoneOffset].getTotalSeconds + case StandardType.DurationType => + val duration = a.asInstanceOf[java.time.Duration] + duration.toString + case StandardType.InstantType => + val instant = a.asInstanceOf[java.time.Instant] + instant.toString + case StandardType.LocalDateType => + val localDate = a.asInstanceOf[java.time.LocalDate] + localDate.toString + case StandardType.LocalTimeType => + val localTime = a.asInstanceOf[java.time.LocalTime] + localTime.toString + case StandardType.LocalDateTimeType => + val localDateTime = a.asInstanceOf[java.time.LocalDateTime] + localDateTime.toString + case StandardType.OffsetTimeType => + val offsetTime = a.asInstanceOf[java.time.OffsetTime] + offsetTime.toString + case StandardType.OffsetDateTimeType => + val offsetDateTime = a.asInstanceOf[java.time.OffsetDateTime] + offsetDateTime.toString + case StandardType.ZonedDateTimeType => + val zonedDateTime = a.asInstanceOf[java.time.ZonedDateTime] + zonedDateTime.toString + } - annotations.collectFirst { case AvroAnnotations.namespace(ns) => ns } match { - case Some(s) => - s match { - case validNamespaceRegex(_) => Right(Some(s)) - case _ => Left(s"Invalid Avro namespace: $s") - } - case None => Right(None) + private def encodeSequence[A](schema: Schema[A], v: Chunk[A]): Any = { + val array = new Array[Any](v.size) + v.zipWithIndex.foreach { + case (a, i) => + array(i) = encodeValue(a, schema) } + java.util.Arrays.asList(array: _*) + } - private[codec] def isErrorRecord(record: Record[_]): Boolean = - record.annotations.collectFirst { case AvroAnnotations.error => () }.nonEmpty - - private[codec] def addNameAnnotationIfMissing[B <: StaticAnnotation](schema: Schema[_], name: String): Schema[_] = - schema.annotations.collectFirst { case AvroAnnotations.name(_) => schema } - .getOrElse(schema.annotate(AvroAnnotations.name(name))) - - private[codec] def toZioDecimal( - avroSchema: SchemaAvro, - decimalType: DecimalType - ): scala.util.Either[String, Schema[_]] = { - val decimalTypeAnnotation = AvroAnnotations.decimal(decimalType) - val decimalLogicalType = avroSchema.getLogicalType.asInstanceOf[LogicalTypes.Decimal] - val precision = decimalLogicalType.getPrecision - val scale = decimalLogicalType.getScale - if (precision - scale > 0) { - Right( - Schema - .primitive(StandardType.BigDecimalType) - .annotate(AvroAnnotations.scale(scale)) - .annotate(AvroAnnotations.precision(precision)) - .annotate(decimalTypeAnnotation) - ) - } else { - Right( - Schema - .primitive(StandardType.BigIntegerType) - .annotate(AvroAnnotations.scale(scale)) - .annotate(decimalTypeAnnotation) - ) + private def encodeSet[A](schema: Schema[A], v: scala.collection.immutable.Set[A]): Any = { + val array = new Array[Any](v.size) + v.zipWithIndex.foreach { + case (a, i) => + array(i) = encodeValue(a, schema) } + java.util.Arrays.asList(array: _*) } - private[codec] def toZioEnumeration[A, Z](avroSchema: SchemaAvro): scala.util.Either[String, Schema[Z]] = { - val cases = avroSchema.getTypes.asScala - .map(t => { - val inner = - if (t.getType == SchemaAvro.Type.RECORD && t.getFields.size() == 1 && t - .getObjectProp(UnionWrapper.propName) == true) { - t.getFields.asScala.head.schema() // unwrap nested union - } else t - toZioSchema(inner).map( - s => - Schema.Case[Z, A]( - t.getFullName, - s.asInstanceOf[Schema[A]], - _.asInstanceOf[A], - _.asInstanceOf[Z], - (z: Z) => z.isInstanceOf[A @unchecked] - ) - ) - }) - val caseSet = cases.toList.map(_.merge).partition { - case _: String => true - case _ => false - } match { - case (Nil, right: Seq[Case[_, _] @unchecked]) => - Try { - CaseSet(right: _*).asInstanceOf[CaseSet { type EnumType = Z }] - }.toEither.left.map(_.getMessage) - case (left, _) => Left(left.mkString("\n")) + private def encodeMap[K, V](schema: Schema.Map[K, V], v: Map[K, V]): Any = { + import scala.jdk.CollectionConverters._ + val map = v.map { + case (k, v) => + encodeValue(k, schema.keySchema) -> encodeValue(v, schema.valueSchema) } - caseSet.map(cs => Schema.enumeration(TypeId.parse(avroSchema.getName), cs)) - } - private[codec] def toZioRecord(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = - if (avroSchema.getObjectProp(UnionWrapper.propName) != null) { - avroSchema.getFields.asScala.headOption match { - case Some(value) => toZioSchema(value.schema()) - case None => Left("ZIO schema wrapped record must have a single field") - } - } else if (avroSchema.getObjectProp(EitherWrapper.propName) != null) { - avroSchema.getFields.asScala.headOption match { - case Some(value) => - toZioSchema(value.schema()).flatMap { - case enu: Enum[_] => - enu.cases.toList match { - case first :: second :: Nil => Right(Schema.either(first.schema, second.schema)) - case _ => Left("ZIO schema wrapped either must have exactly two cases") - } - case e: Schema.Either[_, _] => Right(e) - case c: CaseClass0[_] => Right(c) - case c: CaseClass1[_, _] => Right(c) - case c: CaseClass2[_, _, _] => Right(c) - case c: CaseClass3[_, _, _, _] => Right(c) - case c: CaseClass4[_, _, _, _, _] => Right(c) - case c: CaseClass5[_, _, _, _, _, _] => Right(c) - case c: CaseClass6[_, _, _, _, _, _, _] => Right(c) - case c: CaseClass7[_, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass8[_, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass9[_, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass10[_, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass11[_, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass12[_, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass13[_, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass14[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass15[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass16[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass17[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass18[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass19[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass20[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass21[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: CaseClass22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) - case c: Dynamic => Right(c) - case c: GenericRecord => Right(c) - case c: Map[_, _] => Right(c) - case c: Sequence[_, _, _] => Right(c) - case c: Set[_] => Right(c) - case c: Fail[_] => Right(c) - case c: Lazy[_] => Right(c) - case c: Optional[_] => Right(c) - case c: Primitive[_] => Right(c) - case c: Transform[_, _, _] => Right(c) - case c: Tuple2[_, _] => Right(c) + map.asJava - } - case None => Left("ZIO schema wrapped record must have a single field") - } - } else { - val annotations = buildZioAnnotations(avroSchema) - extractZioFields(avroSchema).map { (fs: List[Field[ListMap[String, _], _]]) => - Schema.record(TypeId.parse(avroSchema.getName), fs: _*).addAllAnnotations(annotations) - } - } + } - private def extractZioFields[Z](avroSchema: SchemaAvro): scala.util.Either[String, List[Field[Z, _]]] = - avroSchema.getFields.asScala.map(toZioField).toList.map(_.merge).partition { - case _: String => true - case _ => false - } match { - case (Nil, right: List[Field[Z, _] @unchecked]) => Right(right) - case (left, _) => Left(left.mkString("\n")) - } + private def encodeOption[A](schema: Schema[A], v: Option[A]): Any = + v.map(encodeValue(_, schema)).orNull - private[codec] def toZioField(field: SchemaAvro.Field): scala.util.Either[String, Field[ListMap[String, _], _]] = - toZioSchema(field.schema()) - .map( - (s: Schema[_]) => - Field( - field.name(), - s.asInstanceOf[Schema[Any]], - buildZioAnnotations(field), - get0 = (p: ListMap[String, _]) => p(field.name()), - set0 = (p: ListMap[String, _], v: Any) => p.updated(field.name(), v) - ) - ) + private def encodeEither[A, B](left: Schema[A], right: Schema[B], either: scala.util.Either[A, B]): Any = { + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Either(left, right, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for Either.")) - private[codec] def toZioTuple(schema: SchemaAvro): scala.util.Either[String, Schema[_]] = - for { - _ <- scala.util.Either - .cond(schema.getFields.size() == 2, (), "Tuple must have exactly 2 fields:" + schema.toString(false)) - _1 <- toZioSchema(schema.getFields.get(0).schema()) - _2 <- toZioSchema(schema.getFields.get(1).schema()) - } yield Schema.Tuple2(_1, _2, buildZioAnnotations(schema)) - - private[codec] def buildZioAnnotations(schema: SchemaAvro): Chunk[StaticAnnotation] = { - val name = AvroAnnotations.name(schema.getName) - val namespace = Try { - Option(schema.getNamespace).map(AvroAnnotations.namespace.apply) - }.toOption.flatten - val doc = if (schema.getDoc != null) Some(AvroAnnotations.doc(schema.getDoc)) else None - val aliases = Try { - if (schema.getAliases != null && !schema.getAliases.isEmpty) - Some(AvroAnnotations.aliases(schema.getAliases.asScala.toSet)) - else None - }.toOption.flatten - val error = Try { - if (schema.isError) Some(AvroAnnotations.error) else None - }.toOption.flatten - val default = Try { - if (schema.getEnumDefault != null) Some(AvroAnnotations.default(schema.getEnumDefault)) else None - }.toOption.flatten - Chunk(name) ++ namespace ++ doc ++ aliases ++ error ++ default - } + val record = new GenericRecordBuilder(schema) + val result = either match { + case Left(a) => record.set("value", encodeValue(a, left)) + case Right(b) => record.set("value", encodeValue(b, right)) + } - private[codec] def buildZioAnnotations(field: SchemaAvro.Field): Chunk[Any] = { - val nameAnnotation = Some(AvroAnnotations.name(field.name)) - val docAnnotation = if (field.doc() != null) Some(AvroAnnotations.doc(field.doc)) else None - val aliasesAnnotation = - if (!field.aliases().isEmpty) Some(AvroAnnotations.aliases(field.aliases.asScala.toSet)) else None - val default = Try { - if (field.hasDefaultValue) Some(AvroAnnotations.default(field.defaultVal())) else None - }.toOption.flatten - val orderAnnotation = Some(AvroAnnotations.fieldOrder(FieldOrderType.fromAvroOrder(field.order()))) - val annotations: Seq[StaticAnnotation] = - List(nameAnnotation, docAnnotation, aliasesAnnotation, orderAnnotation, default).flatten - Chunk.fromIterable(annotations) + result.build() } - private[codec] def toZioStringEnum(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = { - val cases = - avroSchema.getEnumSymbols.asScala - .map(s => Schema.Case[String, String](s, Schema[String], identity, identity, _.isInstanceOf[String])) - .toSeq - val caseSet = CaseSet[String](cases: _*).asInstanceOf[Aux[String]] - val enumeration: Schema[String] = Schema.enumeration(TypeId.parse("org.apache.avro.Schema"), caseSet) - Right(enumeration.addAllAnnotations(buildZioAnnotations(avroSchema))) + private def encodeTuple2[A](schema1: Schema[Any], schema2: Schema[Any], a: A) = { + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.Tuple2(schema1, schema2, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for Tuple2.")) + val record = new GenericData.Record(schema) + val tuple = a.asInstanceOf[(Any, Any)] + record.put("_1", encodeValue(tuple._1, schema1)) + record.put("_2", encodeValue(tuple._2, schema2)) + record } - private[codec] case object OptionUnion { - - def unapply(schema: SchemaAvro): Option[SchemaAvro] = - if (schema.getType == SchemaAvro.Type.UNION) { - val types = schema.getTypes - if (types.size == 2) { - if (types.get(0).getType == SchemaAvro.Type.NULL || - types.get(1).getType == SchemaAvro.Type.NULL) { - if (types.get(1).getType != SchemaAvro.Type.NULL) { - Some(types.get(1)) - } else if (types.get(0).getType != SchemaAvro.Type.NULL) { - Some(types.get(0)) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None + private def encodeGenericRecord[A](a: A, typeId: TypeId, structure: FieldSet): Any = { + val schema = AvroSchemaCodec + .encodeToApacheAvro(Schema.GenericRecord(typeId, structure, Chunk.empty)) + .getOrElse(throw new Exception("Avro schema could not be generated for GenericRecord.")) + val record = new GenericData.Record(schema) + val data = a.asInstanceOf[ListMap[String, _]] + structure.toChunk + .map(schema => schema.name -> encodeValue(data(schema.name), schema.schema.asInstanceOf[Schema[Any]])) + .foreach { + case (name, value) => record.put(name, value) } + record } - private case object EitherUnion { - - def unapply(schema: SchemaAvro): Option[(SchemaAvro, SchemaAvro)] = - if (schema.getType == SchemaAvro.Type.UNION && - schema.getObjectProp(EitherWrapper.propName) == EitherWrapper.value) { - val types = schema.getTypes - if (types.size == 2) { - Some(types.get(0) -> types.get(1)) - } else { - None - } - } else { - None - } + private def encodeCaseClass[Z](schemaRaw: Schema[Z], value: Z, fields: (Schema.Field[Z, _])*): Any = { + val schema = AvroSchemaCodec + .encodeToApacheAvro(schemaRaw) + .getOrElse(throw new Exception("Avro schema could not be generated for CaseClass.")) + val record = new GenericData.Record(schema) + fields.foreach { field => + record.put(field.name, encodeValue(field.get(value), field.schema.asInstanceOf[Schema[Any]])) + } + record } - implicit private class SchemaExtensions(schema: Schema[_]) { - - def addAllAnnotations(annotations: Chunk[Any]): Schema[_] = - annotations.foldLeft(schema)((schema, annotation) => schema.annotate(annotation)) - } + private def encodeEnum[Z](schemaRaw: Schema[Z], value: Z, cases: Schema.Case[Z, _]*): Any = { + val schema = AvroSchemaCodec + .encodeToApacheAvro(schemaRaw) + .getOrElse(throw new Exception("Avro schema could not be generated for Enum.")) + val fieldIndex = cases.indexWhere(c => c.deconstructOption(value).isDefined) + if (fieldIndex >= 0) { + val subtypeCase = cases(fieldIndex) + if (schema.getType == SchemaAvro.Type.ENUM) { + GenericData.get.createEnum(schema.getEnumSymbols.get(fieldIndex), schema) + } else { - implicit private class SchemaAvroExtensions(schemaAvro: SchemaAvro) { + encodeValue(subtypeCase.deconstruct(value), subtypeCase.schema.asInstanceOf[Schema[Any]]) - def addMarkerProp(propMarker: AvroPropMarker): SchemaAvro = { - schemaAvro.addProp(propMarker.propName, propMarker.value) - schemaAvro + } + } else { + throw new Exception("Could not find matching case for enum value.") } } + } diff --git a/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala b/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala new file mode 100644 index 000000000..bb64aeac7 --- /dev/null +++ b/zio-schema-avro/shared/src/main/scala/zio/schema/codec/AvroSchemaCodec.scala @@ -0,0 +1,961 @@ +package zio.schema.codec + +import java.nio.charset.StandardCharsets +import java.time.format.DateTimeFormatter +import java.time.{ Duration, Month, MonthDay, Period, Year, YearMonth } + +import scala.annotation.StaticAnnotation +import scala.collection.immutable.ListMap +import scala.jdk.CollectionConverters._ +import scala.util.{ Right, Try } + +import org.apache.avro.{ LogicalTypes, Schema => SchemaAvro } + +import zio.Chunk +import zio.schema.CaseSet.Aux +import zio.schema.Schema.{ Record, _ } +import zio.schema._ +import zio.schema.codec.AvroAnnotations._ +import zio.schema.codec.AvroPropMarker._ +import zio.schema.meta.MetaSchema + +trait AvroSchemaCodec { + def encode(schema: Schema[_]): scala.util.Either[String, String] + + def decode(bytes: Chunk[Byte]): scala.util.Either[String, Schema[_]] +} + +object AvroSchemaCodec extends AvroSchemaCodec { + + def encode(schema: Schema[_]): scala.util.Either[String, String] = + toAvroSchema(schema).map(_.toString) + + def encodeToApacheAvro(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = + toAvroSchema(schema) + + def decode(bytes: Chunk[Byte]): scala.util.Either[String, Schema[_]] = { + val avroSchemaParser = new SchemaAvro.Parser() + val avroSchema = Try { + avroSchemaParser.parse(new String(bytes.toArray, StandardCharsets.UTF_8)) + }.fold( + e => Left(e.getMessage), + s => Right(s) + ) + avroSchema.flatMap(toZioSchema) + } + + def decodeFromApacheAvro: SchemaAvro => scala.util.Either[String, Schema[_]] = toZioSchema + + private def toZioSchema(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = + for { + // make sure to parse logical types with throwing exceptions enabled, + // otherwise parsing errors on invalid logical types might be lost + _ <- Try { + LogicalTypes.fromSchema(avroSchema) + }.toEither.left.map(e => e.getMessage) + result <- avroSchema.getType match { + case SchemaAvro.Type.RECORD => + RecordType.fromAvroRecord(avroSchema) match { + case Some(RecordType.Period) => Right(Schema.primitive(StandardType.PeriodType)) + case Some(RecordType.YearMonth) => Right(Schema.primitive(StandardType.YearMonthType)) + case Some(RecordType.Tuple) => toZioTuple(avroSchema) + case Some(RecordType.MonthDay) => Right(Schema.primitive(StandardType.MonthDayType)) + case Some(RecordType.Duration) => Right(Schema.primitive(StandardType.DurationType)) + case None => toZioRecord(avroSchema) + } + case SchemaAvro.Type.ENUM => toZioStringEnum(avroSchema) + case SchemaAvro.Type.ARRAY => + toZioSchema(avroSchema.getElementType).map(Schema.list(_)) + case SchemaAvro.Type.MAP => + toZioSchema(avroSchema.getValueType).map(Schema.map(Schema.primitive(StandardType.StringType), _)) + case SchemaAvro.Type.UNION => + avroSchema match { + case OptionUnion(optionSchema) => toZioSchema(optionSchema).map(Schema.option(_)) + case EitherUnion(left, right) => + toZioSchema(left).flatMap(l => toZioSchema(right).map(r => Schema.either(l, r))) + case _ => toZioEnumeration(avroSchema) + } + case SchemaAvro.Type.FIXED => + val fixed = if (avroSchema.getLogicalType == null) { + Right(Schema.primitive(StandardType.BinaryType)) + } else if (avroSchema.getLogicalType.isInstanceOf[LogicalTypes.Decimal]) { + val size = avroSchema.getFixedSize + toZioDecimal(avroSchema, DecimalType.Fixed(size)) + } else { + // TODO: Java implementation of Apache Avro does not support logical type Duration yet: + // AVRO-2123 with PR https://github.com/apache/avro/pull/1263 + Left(s"Unsupported fixed logical type ${avroSchema.getLogicalType}") + } + fixed.map(_.addAllAnnotations(buildZioAnnotations(avroSchema))) + case SchemaAvro.Type.STRING => + StringType.fromAvroString(avroSchema) match { + case Some(stringType) => + val dateTimeFormatter = Formatter.fromAvroStringOrDefault(avroSchema, stringType) + dateTimeFormatter + .map(_.dateTimeFormatter) + .flatMap(_ => { + stringType match { + case StringType.ZoneId => Right(Schema.primitive(StandardType.ZoneIdType)) + case StringType.Instant => + Right( + Schema + .primitive(StandardType.InstantType) + .annotate(AvroAnnotations.formatToString) + ) + case StringType.LocalDate => + Right( + Schema + .primitive(StandardType.LocalDateType) + .annotate(AvroAnnotations.formatToString) + ) + case StringType.LocalTime => + Right( + Schema + .primitive(StandardType.LocalTimeType) + .annotate(AvroAnnotations.formatToString) + ) + case StringType.LocalDateTime => + Right( + Schema + .primitive(StandardType.LocalDateTimeType) + .annotate(AvroAnnotations.formatToString) + ) + case StringType.OffsetTime => + Right(Schema.primitive(StandardType.OffsetTimeType)) + case StringType.OffsetDateTime => + Right(Schema.primitive(StandardType.OffsetDateTimeType)) + case StringType.ZoneDateTime => + Right(Schema.primitive(StandardType.ZonedDateTimeType)) + } + }) + case None => + if (avroSchema.getLogicalType == null) { + Right(Schema.primitive(StandardType.StringType)) + } else if (avroSchema.getLogicalType.getName == LogicalTypes.uuid().getName) { + Right(Schema.primitive(StandardType.UUIDType)) + } else { + Left(s"Unsupported string logical type: ${avroSchema.getLogicalType.getName}") + } + } + case SchemaAvro.Type.BYTES => + if (avroSchema.getLogicalType == null) { + Right(Schema.primitive(StandardType.BinaryType)) + } else if (avroSchema.getLogicalType.isInstanceOf[LogicalTypes.Decimal]) { + toZioDecimal(avroSchema, DecimalType.Bytes) + } else { + Left(s"Unsupported bytes logical type ${avroSchema.getLogicalType.getName}") + } + case SchemaAvro.Type.INT => + IntType.fromAvroInt(avroSchema) match { + case Some(IntType.Char) => Right(Schema.primitive(StandardType.CharType)) + case Some(IntType.DayOfWeek) => Right(Schema.primitive(StandardType.DayOfWeekType)) + case Some(IntType.Year) => Right(Schema.primitive(StandardType.YearType)) + case Some(IntType.Short) => Right(Schema.primitive(StandardType.ShortType)) + case Some(IntType.Month) => Right(Schema.primitive(StandardType.MonthType)) + case Some(IntType.ZoneOffset) => Right(Schema.primitive(StandardType.ZoneOffsetType)) + case None => + if (avroSchema.getLogicalType == null) { + Right(Schema.primitive(StandardType.IntType)) + } else + avroSchema.getLogicalType match { + case _: LogicalTypes.TimeMillis => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.LocalTimeType) + ) + case _: LogicalTypes.Date => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.LocalDateType) + ) + case _ => Left(s"Unsupported int logical type ${avroSchema.getLogicalType.getName}") + } + } + case SchemaAvro.Type.LONG => + if (avroSchema.getLogicalType == null) { + Right(Schema.primitive(StandardType.LongType)) + } else + avroSchema.getLogicalType match { + case _: LogicalTypes.TimeMicros => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.LocalTimeType) + ) + case _: LogicalTypes.TimestampMillis => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.InstantType) + ) + case _: LogicalTypes.TimestampMicros => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.InstantType) + ) + case _: LogicalTypes.LocalTimestampMillis => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.LocalDateTimeType) + ) + case _: LogicalTypes.LocalTimestampMicros => + val formatter = Formatter.fromAvroStringOrDefault(avroSchema, avroSchema.getLogicalType) + formatter.map( + _ => Schema.primitive(StandardType.LocalDateTimeType) + ) + case _ => Left(s"Unsupported long logical type ${avroSchema.getLogicalType.getName}") + } + case SchemaAvro.Type.FLOAT => Right(Schema.primitive(StandardType.FloatType)) + case SchemaAvro.Type.DOUBLE => Right(Schema.primitive(StandardType.DoubleType)) + case SchemaAvro.Type.BOOLEAN => Right(Schema.primitive(StandardType.BoolType)) + case SchemaAvro.Type.NULL => Right(Schema.primitive(StandardType.UnitType)) + case null => Left(s"Unsupported type ${avroSchema.getType}") + } + } yield result + + def toAvroBinary(schema: Schema[_]): SchemaAvro = + schema.annotations.collectFirst { + case AvroAnnotations.bytes(BytesType.Fixed(size, name, doc, space)) => + SchemaAvro.createFixed(name, doc, space, size) + }.getOrElse(SchemaAvro.create(SchemaAvro.Type.BYTES)) + + private[codec] lazy val monthDayStructure: Seq[Schema.Field[MonthDay, Int]] = Seq( + Schema.Field( + "month", + Schema.Primitive(StandardType.IntType), + get0 = _.getMonthValue, + set0 = (a, b: Int) => a.`with`(Month.of(b)) + ), + Schema.Field( + "day", + Schema.Primitive(StandardType.IntType), + get0 = _.getDayOfMonth, + set0 = (a, b: Int) => a.withDayOfMonth(b) + ) + ) + + private[codec] lazy val periodStructure: Seq[Schema.Field[Period, Int]] = Seq( + Schema + .Field("years", Schema.Primitive(StandardType.IntType), get0 = _.getYears, set0 = (a, b: Int) => a.withYears(b)), + Schema + .Field( + "months", + Schema.Primitive(StandardType.IntType), + get0 = _.getMonths, + set0 = (a, b: Int) => a.withMonths(b) + ), + Schema.Field("days", Schema.Primitive(StandardType.IntType), get0 = _.getDays, set0 = (a, b: Int) => a.withDays(b)) + ) + + private[codec] lazy val yearMonthStructure: Seq[Schema.Field[YearMonth, Int]] = Seq( + Schema.Field( + "year", + Schema.Primitive(StandardType.IntType), + get0 = _.getYear, + set0 = (a, b: Int) => a.`with`(Year.of(b)) + ), + Schema.Field( + "month", + Schema.Primitive(StandardType.IntType), + get0 = _.getMonthValue, + set0 = (a, b: Int) => a.`with`(Month.of(b)) + ) + ) + + private[codec] lazy val durationStructure: Seq[Schema.Field[Duration, _]] = Seq( + Schema.Field( + "seconds", + Schema.Primitive(StandardType.LongType), + get0 = _.getSeconds, + set0 = (a, b: Long) => a.plusSeconds(b) + ), + Schema.Field( + "nanos", + Schema.Primitive(StandardType.IntType), + get0 = _.getNano, + set0 = (a, b: Int) => a.plusNanos(b.toLong) + ) + ) + + private def toAvroSchema(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { + schema match { + case e: Enum[_] => toAvroEnum(e) + case record: Record[_] => toAvroRecord(record) + case map: Schema.Map[_, _] => toAvroMap(map) + case seq: Schema.Sequence[_, _, _] => toAvroSchema(seq.elementSchema).map(SchemaAvro.createArray) + case set: Schema.Set[_] => toAvroSchema(set.elementSchema).map(SchemaAvro.createArray) + case Transform(codec, _, _, _, _) => toAvroSchema(codec) + case Primitive(standardType, _) => + standardType match { + case StandardType.UnitType => Right(SchemaAvro.create(SchemaAvro.Type.NULL)) + case StandardType.StringType => Right(SchemaAvro.create(SchemaAvro.Type.STRING)) + case StandardType.BoolType => Right(SchemaAvro.create(SchemaAvro.Type.BOOLEAN)) + case StandardType.ShortType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Short))) + case StandardType.ByteType => Right(SchemaAvro.create(SchemaAvro.Type.INT)) + case StandardType.IntType => Right(SchemaAvro.create(SchemaAvro.Type.INT)) + case StandardType.LongType => Right(SchemaAvro.create(SchemaAvro.Type.LONG)) + case StandardType.FloatType => Right(SchemaAvro.create(SchemaAvro.Type.FLOAT)) + case StandardType.DoubleType => Right(SchemaAvro.create(SchemaAvro.Type.DOUBLE)) + case StandardType.BinaryType => Right(toAvroBinary(schema)) + case StandardType.CharType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Char))) + case StandardType.UUIDType => + Right(LogicalTypes.uuid().addToSchema(SchemaAvro.create(SchemaAvro.Type.STRING))) + case StandardType.BigDecimalType => toAvroDecimal(schema) + case StandardType.BigIntegerType => toAvroDecimal(schema) + case StandardType.DayOfWeekType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.DayOfWeek))) + case StandardType.MonthType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Month))) + case StandardType.YearType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.Year))) + case StandardType.ZoneIdType => + Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(StringDiscriminator(StringType.ZoneId))) + case StandardType.ZoneOffsetType => + Right(SchemaAvro.create(SchemaAvro.Type.INT).addMarkerProp(IntDiscriminator(IntType.ZoneOffset))) + case StandardType.MonthDayType => + //TODO 1 + Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(RecordDiscriminator(RecordType.MonthDay))) + //Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) + case StandardType.PeriodType => + //TODO 2 + Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(RecordDiscriminator(RecordType.Period))) + //Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) + case StandardType.YearMonthType => + //TODO 3 + Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(RecordDiscriminator(RecordType.YearMonth))) + //toAvroSchema(yearMonthStructure).map(_.addMarkerProp(RecordDiscriminator(RecordType.YearMonth))) + //Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) + case StandardType.DurationType => + // TODO: Java implementation of Apache Avro does not support logical type Duration yet: + // AVRO-2123 with PR https://github.com/apache/avro/pull/1263 + //TODO 4 + //val chronoUnitMarker = + //DurationChronoUnit.fromTemporalUnit(temporalUnit).getOrElse(DurationChronoUnit.default) + //toAvroSchema(durationStructure).map( + // _.addMarkerProp(RecordDiscriminator(RecordType.Duration)).addMarkerProp(chronoUnitMarker)) + Right(SchemaAvro.create(SchemaAvro.Type.STRING).addMarkerProp(RecordDiscriminator(RecordType.Duration))) + //Right(SchemaAvro.create(SchemaAvro.Type.RECORD)) + + case StandardType.InstantType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.Instant)) + ) + case StandardType.LocalDateType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalDate)) + ) + case StandardType.LocalTimeType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalTime)) + ) + case StandardType.LocalDateTimeType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalDateTime)) + ) + case StandardType.OffsetTimeType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.OffsetTime)) + ) + case StandardType.OffsetDateTimeType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.OffsetDateTime)) + ) + case StandardType.ZonedDateTimeType => + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.ZoneDateTime)) + ) + } + case Optional(codec, _) => + for { + codecName <- getName(codec) + codecAvroSchema <- toAvroSchema(codec) + wrappedAvroSchema = codecAvroSchema match { + case schema: SchemaAvro if schema.getType == SchemaAvro.Type.NULL => + wrapAvro(schema, codecName, UnionWrapper) + case schema: SchemaAvro if schema.getType == SchemaAvro.Type.UNION => + wrapAvro(schema, codecName, UnionWrapper) + case schema => schema + } + } yield SchemaAvro.createUnion(SchemaAvro.create(SchemaAvro.Type.NULL), wrappedAvroSchema) + case Fail(message, _) => Left(message) + case tuple: Tuple2[_, _] => + toAvroSchema(tuple.toRecord).map( + _.addMarkerProp(RecordDiscriminator(RecordType.Tuple)) + ) + case e @ Schema.Either(left, right, _) => + val eitherUnion = for { + l <- toAvroSchema(left) + r <- toAvroSchema(right) + lname <- getName(left) + rname <- getName(right) + leftSchema = if (l.getType == SchemaAvro.Type.UNION) wrapAvro(l, lname, UnionWrapper) else l + rightSchema = if (r.getType == SchemaAvro.Type.UNION) wrapAvro(r, rname, UnionWrapper) else r + _ <- if (leftSchema.getFullName == rightSchema.getFullName) + Left(s"Left and right schemas of either must have different fullnames: ${leftSchema.getFullName}") + else Right(()) + } yield SchemaAvro.createUnion(leftSchema, rightSchema) + + // Unions in Avro can not hold additional properties, so we need to wrap the union in a record + for { + union <- eitherUnion + name <- getName(e) + } yield wrapAvro(union, name, EitherWrapper) + + case Lazy(schema0) => toAvroSchema(schema0()) + case Dynamic(_) => toAvroSchema(Schema[MetaSchema]) + } + } + + private def hasFormatToStringAnnotation(value: Chunk[Any]) = value.exists { + case AvroAnnotations.formatToString => true + case _ => false + } + + private def getTimeprecisionType(value: Chunk[Any]): Option[TimePrecisionType] = value.collectFirst { + case AvroAnnotations.timeprecision(precision) => precision + } + + private[codec] def toAvroInstant( + formatter: DateTimeFormatter, + annotations: Chunk[Any] + ): scala.util.Either[String, SchemaAvro] = + if (hasFormatToStringAnnotation(annotations)) { + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.Instant)) + .addMarkerProp(Formatter(formatter)) + ) + } else { + val baseSchema = SchemaAvro.create(SchemaAvro.Type.LONG) + getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { + case TimePrecisionType.Millis => + Right(LogicalTypes.timestampMillis().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) + case TimePrecisionType.Micros => + Right(LogicalTypes.timestampMicros().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) + } + } + + private[codec] def toAvroLocalDate( + formatter: DateTimeFormatter, + annotations: Chunk[Any] + ): scala.util.Either[String, SchemaAvro] = + if (hasFormatToStringAnnotation(annotations)) { + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalDate)) + .addMarkerProp(Formatter(formatter)) + ) + } else { + Right(LogicalTypes.date().addToSchema(SchemaAvro.create(SchemaAvro.Type.INT)).addMarkerProp(Formatter(formatter))) + } + + private[codec] def toAvroLocalTime( + formatter: DateTimeFormatter, + annotations: Chunk[Any] + ): scala.util.Either[String, SchemaAvro] = + if (hasFormatToStringAnnotation(annotations)) { + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalTime)) + .addMarkerProp(Formatter(formatter)) + ) + } else { + getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { + case TimePrecisionType.Millis => + Right( + LogicalTypes + .timeMillis() + .addToSchema(SchemaAvro.create(SchemaAvro.Type.INT)) + .addMarkerProp(Formatter(formatter)) + ) + case TimePrecisionType.Micros => + Right( + LogicalTypes + .timeMicros() + .addToSchema(SchemaAvro.create(SchemaAvro.Type.LONG)) + .addMarkerProp(Formatter(formatter)) + ) + } + } + + private[codec] def toAvroLocalDateTime( + formatter: DateTimeFormatter, + annotations: Chunk[Any] + ): scala.util.Either[String, SchemaAvro] = + if (hasFormatToStringAnnotation(annotations)) { + Right( + SchemaAvro + .create(SchemaAvro.Type.STRING) + .addMarkerProp(StringDiscriminator(StringType.LocalDateTime)) + .addMarkerProp(Formatter(formatter)) + ) + } else { + val baseSchema = SchemaAvro.create(SchemaAvro.Type.LONG) + getTimeprecisionType(annotations).getOrElse(TimePrecisionType.default) match { + case TimePrecisionType.Millis => + Right(LogicalTypes.localTimestampMillis().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) + case TimePrecisionType.Micros => + Right(LogicalTypes.localTimestampMicros().addToSchema(baseSchema).addMarkerProp(Formatter(formatter))) + } + } + + def hasAvroEnumAnnotation(annotations: Chunk[Any]): Boolean = annotations.exists { + case AvroAnnotations.avroEnum() => true + case _ => false + } + + def wrapAvro(schemaAvro: SchemaAvro, name: String, marker: AvroPropMarker): SchemaAvro = { + val field = new SchemaAvro.Field("value", schemaAvro) + val fields = new java.util.ArrayList[SchemaAvro.Field]() + fields.add(field) + val prefixedName = s"${AvroPropMarker.wrapperNamePrefix}_$name" + SchemaAvro + .createRecord(prefixedName, null, AvroPropMarker.wrapperNamespace, false, fields) + .addMarkerProp(marker) + } + + private[codec] def toAvroEnum(enu: Enum[_]): scala.util.Either[String, SchemaAvro] = { + val avroEnumAnnotationExists = hasAvroEnumAnnotation(enu.annotations) + val isAvroEnumEquivalent = enu.cases.map(_.schema).forall { + case (Transform(Primitive(standardType, _), _, _, _, _)) + if standardType == StandardType.UnitType && avroEnumAnnotationExists => + true + case (Primitive(standardType, _)) if standardType == StandardType.StringType => true + case (CaseClass0(_, _, _)) if avroEnumAnnotationExists => true + case _ => false + } + if (isAvroEnumEquivalent) { + for { + name <- getName(enu) + doc = getDoc(enu.annotations).orNull + namespaceOption <- getNamespace(enu.annotations) + symbols = enu.cases.map { + case caseValue => getNameOption(caseValue.annotations).getOrElse(caseValue.id) + }.toList + result = SchemaAvro.createEnum(name, doc, namespaceOption.orNull, symbols.asJava) + } yield result + } else { + val cases = enu.cases.map(c => (c.id, (c.schema, c.annotations))).map { + case (symbol, (Transform(Primitive(standardType, _), _, _, _, _), annotations)) + if standardType == StandardType.UnitType => + val name = getNameOption(annotations).getOrElse(symbol) + Right(SchemaAvro.createRecord(name, null, null, false, new java.util.ArrayList[SchemaAvro.Field])) + case (symbol, (CaseClass0(_, _, _), annotations)) => + val name = getNameOption(annotations).getOrElse(symbol) + Right(SchemaAvro.createRecord(name, null, null, false, new java.util.ArrayList[SchemaAvro.Field])) + case (symbol, (schema, annotations)) => + val name = getNameOption(annotations).getOrElse(symbol) + val schemaWithName = addNameAnnotationIfMissing(schema, name) + toAvroSchema(schemaWithName).map { + case schema: SchemaAvro if schema.getType == SchemaAvro.Type.UNION => + wrapAvro(schema, name, UnionWrapper) // handle nested unions + case schema => schema + } + } + cases.toList.map(_.merge).partition { + case _: String => true + case _ => false + } match { + case (Nil, right: List[org.apache.avro.Schema @unchecked]) => Right(SchemaAvro.createUnion(right.asJava)) + case (left, _) => Left(left.mkString("\n")) + } + } + } + + private def extractAvroFields(record: Record[_]): List[org.apache.avro.Schema.Field] = + record.fields.map(toAvroRecordField).toList.map(_.merge).partition { + case _: String => true + case _ => false + } match { + case (Nil, right: List[org.apache.avro.Schema.Field @unchecked]) => right + case _ => null + } + + private[codec] def toAvroRecord(record: Record[_]): scala.util.Either[String, SchemaAvro] = + for { + name <- getName(record) + namespaceOption <- getNamespace(record.annotations) + result <- Right( + SchemaAvro.createRecord( + name, + getDoc(record.annotations).orNull, + namespaceOption.orNull, + isErrorRecord(record), + extractAvroFields(record).asJava + ) + ) + } yield result + + private[codec] def toAvroMap(map: Map[_, _]): scala.util.Either[String, SchemaAvro] = + map.keySchema match { + case p: Schema.Primitive[_] if p.standardType == StandardType.StringType => + toAvroSchema(map.valueSchema).map(SchemaAvro.createMap) + case _ => + val tupleSchema = Schema + .Tuple2(map.keySchema, map.valueSchema) + .annotate(AvroAnnotations.name("Tuple")) + .annotate(AvroAnnotations.namespace("scala")) + toAvroSchema(tupleSchema).map(SchemaAvro.createArray) + } + + private[codec] def toAvroDecimal(schema: Schema[_]): scala.util.Either[String, SchemaAvro] = { + val scale = schema.annotations.collectFirst { case AvroAnnotations.scale(s) => s } + .getOrElse(AvroAnnotations.scale().scale) + val precision = schema match { + case Primitive(StandardType.BigDecimalType, _) => + schema.annotations.collectFirst { case AvroAnnotations.precision(p) => p } + .getOrElse(Math.max(scale, AvroAnnotations.precision().precision)) + case _ => scale + } + + val baseAvroType = schema.annotations.collectFirst { case AvroAnnotations.decimal(decimalType) => decimalType } + .getOrElse(DecimalType.default) match { + case DecimalType.Fixed(size) => + for { + namespaceOption <- getNamespace(schema.annotations) + name = getNameOption(schema.annotations).getOrElse(s"Decimal_${precision}_$scale") + doc = getDoc(schema.annotations).orNull + result = SchemaAvro.createFixed(name, doc, namespaceOption.orNull, size) + } yield result + case DecimalType.Bytes => Right(SchemaAvro.create(SchemaAvro.Type.BYTES)) + } + baseAvroType.map( + LogicalTypes + .decimal(precision, scale) + .addToSchema(_) + ) + } + + private[codec] def toErrorMessage(err: Throwable, at: AnyRef) = + s"Error mapping to Apache Avro schema: $err at ${at.toString}" + + private[codec] def toAvroRecordField[Z](value: Field[Z, _]): scala.util.Either[String, SchemaAvro.Field] = + toAvroSchema(value.schema).map( + schema => + new SchemaAvro.Field( + getNameOption(value.annotations).getOrElse(value.name), + schema, + getDoc(value.annotations).orNull, + getDefault(value.annotations).orNull, + getFieldOrder(value.annotations).map(_.toAvroOrder).getOrElse(FieldOrderType.default.toAvroOrder) + ) + ) + + private[codec] def getFieldOrder(annotations: Chunk[Any]): Option[FieldOrderType] = + annotations.collectFirst { case AvroAnnotations.fieldOrder(fieldOrderType) => fieldOrderType } + + private[codec] def getName(schema: Schema[_]): scala.util.Either[String, String] = { + val validNameRegex = raw"[A-Za-z_][A-Za-z0-9_]*".r + + schema.annotations.collectFirst { case AvroAnnotations.name(name) => name } match { + case Some(s) => + s match { + case validNameRegex() => Right(s) + case _ => + Left(s"Invalid Avro name: $s") + } + case None => + schema match { + case r: Record[_] => Right(r.id.name) + case e: Enum[_] => Right(e.id.name) + case _ => Right(s"hashed_${schema.ast.toString.hashCode().toString.replaceFirst("-", "n")}") + // TODO: better way to generate a (maybe stable) name? + } + } + } + + private[codec] def getNameOption(annotations: Chunk[Any]): Option[String] = + annotations.collectFirst { case AvroAnnotations.name(name) => name } + + private[codec] def getDoc(annotations: Chunk[Any]): Option[String] = + annotations.collectFirst { case AvroAnnotations.doc(doc) => doc } + + private[codec] def getDefault(annotations: Chunk[Any]): Option[java.lang.Object] = + annotations.collectFirst { case AvroAnnotations.default(javaDefaultObject) => javaDefaultObject } + + private[codec] def getNamespace(annotations: Chunk[Any]): scala.util.Either[String, Option[String]] = { + val validNamespaceRegex = raw"[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*".r + + annotations.collectFirst { case AvroAnnotations.namespace(ns) => ns } match { + case Some(s) => + s match { + case validNamespaceRegex(_) => Right(Some(s)) + case _ => Left(s"Invalid Avro namespace: $s") + } + case None => Right(None) + } + } + + private[codec] def isErrorRecord(record: Record[_]): Boolean = + record.annotations.collectFirst { case AvroAnnotations.error => () }.nonEmpty + + private[codec] def addNameAnnotationIfMissing[B <: StaticAnnotation](schema: Schema[_], name: String): Schema[_] = + schema.annotations.collectFirst { case AvroAnnotations.name(_) => schema } + .getOrElse(schema.annotate(AvroAnnotations.name(name))) + + private[codec] def toZioDecimal( + avroSchema: SchemaAvro, + decimalType: DecimalType + ): scala.util.Either[String, Schema[_]] = { + val decimalTypeAnnotation = AvroAnnotations.decimal(decimalType) + val decimalLogicalType = avroSchema.getLogicalType.asInstanceOf[LogicalTypes.Decimal] + val precision = decimalLogicalType.getPrecision + val scale = decimalLogicalType.getScale + if (precision - scale > 0) { + Right( + Schema + .primitive(StandardType.BigDecimalType) + .annotate(AvroAnnotations.scale(scale)) + .annotate(AvroAnnotations.precision(precision)) + .annotate(decimalTypeAnnotation) + ) + } else { + Right( + Schema + .primitive(StandardType.BigIntegerType) + .annotate(AvroAnnotations.scale(scale)) + .annotate(decimalTypeAnnotation) + ) + } + } + + private[codec] def toZioEnumeration[A, Z](avroSchema: SchemaAvro): scala.util.Either[String, Schema[Z]] = { + val cases = avroSchema.getTypes.asScala + .map(t => { + val inner = + if (t.getType == SchemaAvro.Type.RECORD && t.getFields.size() == 1 && t + .getObjectProp(UnionWrapper.propName) == true) { + t.getFields.asScala.head.schema() // unwrap nested union + } else t + toZioSchema(inner).map( + s => + Schema.Case[Z, A]( + t.getFullName, + s.asInstanceOf[Schema[A]], + _.asInstanceOf[A], + _.asInstanceOf[Z], + (z: Z) => z.isInstanceOf[A @unchecked] + ) + ) + }) + val caseSet = cases.toList.map(_.merge).partition { + case _: String => true + case _ => false + } match { + case (Nil, right: Seq[Case[_, _] @unchecked]) => + Try { + CaseSet(right: _*).asInstanceOf[CaseSet { type EnumType = Z }] + }.toEither.left.map(_.getMessage) + case (left, _) => Left(left.mkString("\n")) + } + caseSet.map(cs => Schema.enumeration(TypeId.parse(avroSchema.getName), cs)) + } + + private[codec] def toZioRecord(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = + if (avroSchema.getObjectProp(UnionWrapper.propName) != null) { + avroSchema.getFields.asScala.headOption match { + case Some(value) => toZioSchema(value.schema()) + case None => Left("ZIO schema wrapped record must have a single field") + } + } else if (avroSchema.getObjectProp(EitherWrapper.propName) != null) { + avroSchema.getFields.asScala.headOption match { + case Some(value) => + toZioSchema(value.schema()).flatMap { + case enu: Enum[_] => + enu.cases.toList match { + case first :: second :: Nil => Right(Schema.either(first.schema, second.schema)) + case _ => Left("ZIO schema wrapped either must have exactly two cases") + } + case e: Schema.Either[_, _] => Right(e) + case c: CaseClass0[_] => Right(c) + case c: CaseClass1[_, _] => Right(c) + case c: CaseClass2[_, _, _] => Right(c) + case c: CaseClass3[_, _, _, _] => Right(c) + case c: CaseClass4[_, _, _, _, _] => Right(c) + case c: CaseClass5[_, _, _, _, _, _] => Right(c) + case c: CaseClass6[_, _, _, _, _, _, _] => Right(c) + case c: CaseClass7[_, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass8[_, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass9[_, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass10[_, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass11[_, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass12[_, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass13[_, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass14[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass15[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass16[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass17[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass18[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass19[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass20[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass21[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: CaseClass22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] => Right(c) + case c: Dynamic => Right(c) + case c: GenericRecord => Right(c) + case c: Map[_, _] => Right(c) + case c: Sequence[_, _, _] => Right(c) + case c: Set[_] => Right(c) + case c: Fail[_] => Right(c) + case c: Lazy[_] => Right(c) + case c: Optional[_] => Right(c) + case c: Primitive[_] => Right(c) + case c: Transform[_, _, _] => Right(c) + case c: Tuple2[_, _] => Right(c) + + } + case None => Left("ZIO schema wrapped record must have a single field") + } + } else { + val annotations = buildZioAnnotations(avroSchema) + extractZioFields(avroSchema).map { (fs: List[Field[ListMap[String, _], _]]) => + Schema.record(TypeId.parse(avroSchema.getName), fs: _*).addAllAnnotations(annotations) + } + } + + private def extractZioFields[Z](avroSchema: SchemaAvro): scala.util.Either[String, List[Field[Z, _]]] = + avroSchema.getFields.asScala.map(toZioField).toList.map(_.merge).partition { + case _: String => true + case _ => false + } match { + case (Nil, right: List[Field[Z, _] @unchecked]) => Right(right) + case (left, _) => Left(left.mkString("\n")) + } + + private[codec] def toZioField(field: SchemaAvro.Field): scala.util.Either[String, Field[ListMap[String, _], _]] = + toZioSchema(field.schema()) + .map( + (s: Schema[_]) => + Field( + field.name(), + s.asInstanceOf[Schema[Any]], + buildZioAnnotations(field), + get0 = (p: ListMap[String, _]) => p(field.name()), + set0 = (p: ListMap[String, _], v: Any) => p.updated(field.name(), v) + ) + ) + + private[codec] def toZioTuple(schema: SchemaAvro): scala.util.Either[String, Schema[_]] = + for { + _ <- scala.util.Either + .cond(schema.getFields.size() == 2, (), "Tuple must have exactly 2 fields:" + schema.toString(false)) + _1 <- toZioSchema(schema.getFields.get(0).schema()) + _2 <- toZioSchema(schema.getFields.get(1).schema()) + } yield Schema.Tuple2(_1, _2, buildZioAnnotations(schema)) + + private[codec] def buildZioAnnotations(schema: SchemaAvro): Chunk[StaticAnnotation] = { + val name = AvroAnnotations.name(schema.getName) + val namespace = Try { + Option(schema.getNamespace).map(AvroAnnotations.namespace.apply) + }.toOption.flatten + val doc = if (schema.getDoc != null) Some(AvroAnnotations.doc(schema.getDoc)) else None + val aliases = Try { + if (schema.getAliases != null && !schema.getAliases.isEmpty) + Some(AvroAnnotations.aliases(schema.getAliases.asScala.toSet)) + else None + }.toOption.flatten + val error = Try { + if (schema.isError) Some(AvroAnnotations.error) else None + }.toOption.flatten + val default = Try { + if (schema.getEnumDefault != null) Some(AvroAnnotations.default(schema.getEnumDefault)) else None + }.toOption.flatten + Chunk(name) ++ namespace ++ doc ++ aliases ++ error ++ default + } + + private[codec] def buildZioAnnotations(field: SchemaAvro.Field): Chunk[Any] = { + val nameAnnotation = Some(AvroAnnotations.name(field.name)) + val docAnnotation = if (field.doc() != null) Some(AvroAnnotations.doc(field.doc)) else None + val aliasesAnnotation = + if (!field.aliases().isEmpty) Some(AvroAnnotations.aliases(field.aliases.asScala.toSet)) else None + val default = Try { + if (field.hasDefaultValue) Some(AvroAnnotations.default(field.defaultVal())) else None + }.toOption.flatten + val orderAnnotation = Some(AvroAnnotations.fieldOrder(FieldOrderType.fromAvroOrder(field.order()))) + val annotations: Seq[StaticAnnotation] = + List(nameAnnotation, docAnnotation, aliasesAnnotation, orderAnnotation, default).flatten + Chunk.fromIterable(annotations) + } + + private[codec] def toZioStringEnum(avroSchema: SchemaAvro): scala.util.Either[String, Schema[_]] = { + val cases = + avroSchema.getEnumSymbols.asScala + .map(s => Schema.Case[String, String](s, Schema[String], identity, identity, _.isInstanceOf[String])) + .toSeq + val caseSet = CaseSet[String](cases: _*).asInstanceOf[Aux[String]] + val enumeration: Schema[String] = Schema.enumeration(TypeId.parse("org.apache.avro.Schema"), caseSet) + Right(enumeration.addAllAnnotations(buildZioAnnotations(avroSchema))) + } + + private[codec] case object OptionUnion { + + def unapply(schema: SchemaAvro): Option[SchemaAvro] = + if (schema.getType == SchemaAvro.Type.UNION) { + val types = schema.getTypes + if (types.size == 2) { + if (types.get(0).getType == SchemaAvro.Type.NULL || + types.get(1).getType == SchemaAvro.Type.NULL) { + if (types.get(1).getType != SchemaAvro.Type.NULL) { + Some(types.get(1)) + } else if (types.get(0).getType != SchemaAvro.Type.NULL) { + Some(types.get(0)) + } else { + None + } + } else { + None + } + } else { + None + } + } else { + None + } + } + + private case object EitherUnion { + + def unapply(schema: SchemaAvro): Option[(SchemaAvro, SchemaAvro)] = + if (schema.getType == SchemaAvro.Type.UNION && + schema.getObjectProp(EitherWrapper.propName) == EitherWrapper.value) { + val types = schema.getTypes + if (types.size == 2) { + Some(types.get(0) -> types.get(1)) + } else { + None + } + } else { + None + } + } + + implicit private class SchemaExtensions(schema: Schema[_]) { + + def addAllAnnotations(annotations: Chunk[Any]): Schema[_] = + annotations.foldLeft(schema)((schema, annotation) => schema.annotate(annotation)) + } + + implicit private class SchemaAvroExtensions(schemaAvro: SchemaAvro) { + + def addMarkerProp(propMarker: AvroPropMarker): SchemaAvro = { + schemaAvro.addProp(propMarker.propName, propMarker.value) + schemaAvro + } + } +} diff --git a/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala b/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala index 3592028f2..8f1526aaf 100644 --- a/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala +++ b/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroCodecSpec.scala @@ -1,2051 +1,710 @@ package zio.schema.codec +import java.math.BigInteger +import java.time.{ + DayOfWeek, + Instant, + LocalDate, + LocalDateTime, + LocalTime, + Month, + MonthDay, + OffsetDateTime, + OffsetTime, + Period, + Year, + YearMonth, + ZoneId, + ZoneOffset, + ZonedDateTime +} import java.util.UUID -import scala.collection.immutable.ListMap -import scala.util.Try +import org.apache.avro.generic.GenericData -import zio.schema.Schema._ -import zio.schema._ -import zio.schema.codec.AvroAnnotations.{ BytesType, DecimalType, FieldOrderType } -import zio.test.Assertion._ +import zio._ +import zio.schema.codec.AvroAnnotations.avroEnum +import zio.schema.{ DeriveSchema, Schema } +import zio.stream.ZStream import zio.test._ -import zio.{ Chunk, Scope } object AvroCodecSpec extends ZIOSpecDefault { - import AssertionHelper._ - import SpecTestData._ - - override def spec: Spec[Environment with TestEnvironment with Scope, Any] = - suite("AvroCodecSpec")( - suite("encode")( - suite("enum")( - test("encodes string only enum as avro enum") { - val caseA = Schema.Case[String, String]( - "A", - Schema.primitive(StandardType.StringType), - identity, - identity, - _.isInstanceOf[String] - ) - val caseB = Schema.Case[String, String]( - "B", - Schema.primitive(StandardType.StringType), - identity, - identity, - _.isInstanceOf[String] - ) - val caseC = Schema.Case[String, String]( - "C", - Schema.primitive(StandardType.StringType), - identity, - identity, - _.isInstanceOf[String] - ) - val schema = Schema.Enum3(TypeId.Structural, caseA, caseB, caseC, Chunk(AvroAnnotations.name("MyEnum"))) - - val result = AvroCodec.encode(schema) - - val expected = """{"type":"enum","name":"MyEnum","symbols":["A","B","C"]}""" - assert(result)(isRight(equalTo(expected))) - }, - test("encodes sealed trait objects only as union of records when no avroEnum annotation is present") { - - val schema = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt] - val result = AvroCodec.encode(schema) - - val expected = - """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" - assert(result)(isRight(equalTo(expected))) - }, - test("encodes sealed trait objects only as enum when avroEnum annotation is present") { - - val schema = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt].annotate(AvroAnnotations.avroEnum) - val result = AvroCodec.encode(schema) - - val expected = """{"type":"enum","name":"MyEnum","symbols":["A","B","MyC"]}""" - assert(result)(isRight(equalTo(expected))) - }, - test("ignores avroEnum annotation if ADT cannot be reduced to String symbols") { - val schema = DeriveSchema.gen[SpecTestData.CaseObjectAndCaseClassAdt] - val result = AvroCodec.encode(schema) - - val expected = - """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" - assert(result)(isRight(equalTo(expected))) - }, - test("flatten nested unions with initialSchemaDerived derivation") { - val schema = DeriveSchema.gen[SpecTestData.UnionWithNesting] - val result = AvroCodec.encode(schema) - - val expected = - """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" - assert(result)(isRight(equalTo(expected))) - }, - test("wraps nested unions") { - val schemaA = DeriveSchema.gen[UnionWithNesting.Nested.A.type] - val schemaB = DeriveSchema.gen[UnionWithNesting.Nested.B.type] - val schemaC = DeriveSchema.gen[UnionWithNesting.C.type] - val schemaD = DeriveSchema.gen[UnionWithNesting.D] - - val nested: Enum2[UnionWithNesting.Nested.A.type, UnionWithNesting.Nested.B.type, UnionWithNesting.Nested] = - Schema.Enum2( - TypeId.Structural, - Schema.Case[UnionWithNesting.Nested, UnionWithNesting.Nested.A.type]( - "A", - schemaA, - _.asInstanceOf[UnionWithNesting.Nested.A.type], - _.asInstanceOf[UnionWithNesting.Nested], - _.isInstanceOf[UnionWithNesting.Nested.A.type] - ), - Schema.Case[UnionWithNesting.Nested, UnionWithNesting.Nested.B.type]( - "B", - schemaB, - _.asInstanceOf[UnionWithNesting.Nested.B.type], - _.asInstanceOf[UnionWithNesting.Nested], - _.isInstanceOf[UnionWithNesting.Nested.B.type] - ) - ) - val unionWithNesting = Schema.Enum3( - TypeId.Structural, - Schema.Case[UnionWithNesting, UnionWithNesting.Nested]( - "Nested", - nested, - _.asInstanceOf[UnionWithNesting.Nested], - _.asInstanceOf[UnionWithNesting], - _.isInstanceOf[UnionWithNesting.Nested] - ), - Schema - .Case[UnionWithNesting, UnionWithNesting.C.type]( - "C", - schemaC, - _.asInstanceOf[UnionWithNesting.C.type], - _.asInstanceOf[UnionWithNesting], - _.isInstanceOf[UnionWithNesting.C.type] - ), - Schema.Case[UnionWithNesting, UnionWithNesting.D]( - "D", - schemaD, - _.asInstanceOf[UnionWithNesting.D], - _.asInstanceOf[UnionWithNesting], - _.isInstanceOf[UnionWithNesting.D] - ) - ) - - val schema = unionWithNesting - val result = AvroCodec.encode(schema) - - val wrappedString = - """[{"type":"record","name":"wrapper_Nested","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true},{"type":"record","name":"C","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" - assert(result)(isRight(equalTo(wrappedString))) - } - ), - suite("record")( - test("generate a static name if not specified via annotation") { - val schema1 = DeriveSchema.gen[SpecTestData.Record] - val schema2 = DeriveSchema.gen[SpecTestData.Record] - val result1 = AvroCodec.encode(schema1) - val result2 = AvroCodec.encode(schema2) - - val expected = - """{"type":"record","name":"hashed_1642816955","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - assert(result1)(isRight(equalTo(expected))) && assert(result2)(isRight(equalTo(expected))) - } @@ TestAspect.ignore, // TODO: FIX - test("fail with left on invalid name") { - val schema = DeriveSchema.gen[SpecTestData.Record].annotate(AvroAnnotations.name("0invalid")) - val result = AvroCodec.encode(schema) - - assert(result)(isLeft(containsString("""0invalid"""))) - }, - test("pick up name from annotation") { - val schema = DeriveSchema.gen[SpecTestData.NamedRecord] - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - ) - ) - ) - }, - test("pick up name from annotation for fields") { - val schema = DeriveSchema.gen[SpecTestData.NamedFieldRecord] - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"MyNamedFieldRecord","fields":[{"name":"myNamedField","type":"string"},{"name":"b","type":"boolean"}]}""" - ) - ) - ) - }, - test("pick up doc from annotation") { - val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.doc("My doc")) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"MyNamedRecord","doc":"My doc","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - ) - ) - ) - }, - test("pick up namespace from annotation") { - val schema = - DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.namespace("test.namespace")) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"MyNamedRecord","namespace":"test.namespace","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - ) - ) - ) - }, - test("fail with left on invalid namespace") { - val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.namespace("0@-.invalid")) - val result = AvroCodec.encode(schema) - - assert(result)(isLeft(containsString("""0@-.invalid"""))) - }, - test("pick up error annotation") { - val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.error) - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"error","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - assert(result)(isRight(equalTo(expected))) - }, - test("includes all fields") { - val schema = DeriveSchema.gen[SpecTestData.NamedRecord] - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - assert(result)(isRight(equalTo(expected))) - }, - test("includes nested record fields") { - val schema = DeriveSchema.gen[SpecTestData.NestedRecord] - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"NestedRecord","fields":[{"name":"s","type":"string"},{"name":"nested","type":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" - assert(result)(isRight(equalTo(expected))) - } - ), - suite("map")( - test("string keys and string values") { - val keySchema = Schema.primitive(StandardType.StringType) - val valueSchema = Schema.primitive(StandardType.StringType) - val schema = Schema.Map(keySchema, valueSchema) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"map","values":"string"}"""))) - }, - test("string keys and complex values") { - val keySchema = Schema.primitive(StandardType.StringType) - val valueSchema = DeriveSchema.gen[SpecTestData.SimpleRecord] - val schema = Schema.Map(keySchema, valueSchema) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"map","values":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" - ) - ) - ) - }, - test("complex keys and string values") { - val keySchema = DeriveSchema.gen[SpecTestData.SimpleRecord] - val valueSchema = Schema.primitive(StandardType.StringType) - val schema = Schema.Map(keySchema, valueSchema) - val result = AvroCodec.encode(schema) - - val isArray = startsWithString("""{"type":"array"""") - val tupleItems = containsString(""""items":{"type":"record","name":"Tuple","namespace":"scala"""") - val hasTupleField_1 = containsString( - """{"name":"_1","type":{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]}}""" - ) - val hasTupleField_2 = containsString("""{"name":"_2","type":"string"}""") - - assert(result)(isRight(isArray && tupleItems && hasTupleField_1 && hasTupleField_2)) - }, - test("complex keys and complex values") { - val keySchema = DeriveSchema.gen[SpecTestData.SimpleRecord] - val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] - val schema = Schema.Map(keySchema, valueSchema) - val result = AvroCodec.encode(schema) - - val isArray = startsWithString("""{"type":"array"""") - val tupleItems = containsString(""""items":{"type":"record","name":"Tuple","namespace":"scala"""") - val hasTupleField_1 = containsString( - """{"name":"_1","type":{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]}}""" - ) - val hasTupleField_2 = containsString( - """{"name":"_2","type":{"type":"record","name":"MyNamedRecord","namespace":"","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" - ) - - assert(result)(isRight(isArray && tupleItems && hasTupleField_1 && hasTupleField_2)) - } - ), - suite("seq")( - test("is mapped to an avro array") { - val schema = Schema.Sequence[Chunk[String], String, String]( - Schema.primitive(StandardType.StringType), - identity, - identity, - Chunk.empty, - "Seq" - ) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"array","items":"string"}"""))) - }, - test("encodes complex types") { - val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] - val schema = Schema - .Sequence[Chunk[NamedRecord], NamedRecord, String](valueSchema, identity, identity, Chunk.empty, "Seq") - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"array","items":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" - ) - ) - ) - } - ), - suite("set")( - test("is mapped to an avro array") { - val schema = Schema.Sequence[Chunk[String], String, String]( - Schema.primitive(StandardType.StringType), - identity, - identity, - Chunk.empty, - "Seq" - ) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"array","items":"string"}"""))) - }, - test("encodes complex types") { - val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] - val schema = Schema - .Sequence[Chunk[NamedRecord], NamedRecord, String](valueSchema, identity, identity, Chunk.empty, "Seq") - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"array","items":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" - ) - ) - ) - } - ), - suite("optional")( - test("creates a union with case NULL") { - val schema = Schema.Optional(Schema.primitive(StandardType.StringType)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""["null","string"]"""))) - }, - test("encodes complex types") { - val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] - val schema = Schema.Optional(valueSchema) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """["null",{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}]""" - ) - ) - ) - }, - test("wraps optional of unit to prevent duplicate null in union") { - val schema = Schema.Optional(Schema.primitive(StandardType.UnitType)) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """["null",{"type":"record","name":"wrapper_hashed_3594628","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":"null"}],"zio.schema.codec.avro.wrapper":true}]""" - ) - ) - ) - }, - test("encodes nested optionals") { - val nested = Schema.Optional(Schema.primitive(StandardType.StringType)) - val schema = Schema.Optional(nested) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """["null",{"type":"record","name":"wrapper_hashed_n813828848","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true}]""" - ) - ) - ) - }, - test("encodes optionals of union") { - val union = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt] - val schema = Schema.Optional(union) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """["null",{"type":"record","name":"wrapper_MyEnum","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]},{"type":"record","name":"MyC","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true}]""" - ) - ) - ) - }, - test("encodes optionals of either") { - val either = - Schema.Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) - val schema = Schema.Optional(either) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """["null",{"type":"record","name":"wrapper_hashed_n630422444","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}]""" - ) - ) - ) - } - ), - suite("either")( - test("create an union") { - val schema = - Schema.Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"wrapper_hashed_n630422444","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}""" - assert(result)(isRight(equalTo(expected))) - }, - test("create a named union") { - val schema = Schema - .Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) - .annotate(AvroAnnotations.name("MyEither")) - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"wrapper_MyEither","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}""" - assert(result)(isRight(equalTo(expected))) - }, - test("encodes complex types") { - val left = DeriveSchema.gen[SpecTestData.SimpleRecord] - val right = Schema.primitive(StandardType.StringType) - val schema = Schema.Either(left, right) - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"wrapper_hashed_754352222","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]},"string"]}],"zio.schema.codec.avro.either":true}""" - assert(result)(isRight(equalTo(expected))) - }, - test("fails with duplicate names") { - val left = DeriveSchema.gen[SpecTestData.SimpleRecord] - val right = DeriveSchema.gen[SpecTestData.SimpleRecord] - val schema = Schema.Either(left, right) - val result = AvroCodec.encode(schema) - - assert(result)( - isLeft(equalTo("""Left and right schemas of either must have different fullnames: Simple""")) - ) - }, - test("encodes either containing optional") { - val left = Schema.Optional(Schema.primitive(StandardType.StringType)) - val right = Schema.primitive(StandardType.StringType) - val schema = Schema.Either(left, right) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"wrapper_hashed_n465006219","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true}""" - ) - ) - ) - }, - test("encodes nested either") { - val left = Schema.Optional(Schema.primitive(StandardType.StringType)) - val right = Schema.primitive(StandardType.StringType) - val nested = Schema.Either(left, right) - val schema = Schema.Either(nested, right) - val result = AvroCodec.encode(schema) - - val expected = - """{"type":"record","name":"wrapper_hashed_2071802344","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n465006219","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true},"string"]}],"zio.schema.codec.avro.either":true}""" - assert(result)(isRight(equalTo(expected))) - } - ), - suite("tuple")( - test("creates a record type and applies the name") { - val left = Schema.primitive(StandardType.StringType) - val right = Schema.primitive(StandardType.StringType) - val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"record","name":"MyTuple","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"string"}],"zio.schema.codec.recordType":"tuple"}""" - ) - ) - ) - }, - test("encodes complex types") { - val left = DeriveSchema.gen[SpecTestData.SimpleRecord] - val right = DeriveSchema.gen[SpecTestData.NamedRecord] - val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) - val result = AvroCodec.encode(schema) - - val field_1 = - """{"name":"_1","type":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" - val field_2 = - """{"name":"_2","type":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" - assert(result)(isRight(containsString(field_1) && containsString(field_2))) - }, - test("encodes duplicate complex types by reference") { - val left = DeriveSchema.gen[SpecTestData.SimpleRecord] - val right = DeriveSchema.gen[SpecTestData.SimpleRecord] - val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) - val result = AvroCodec.encode(schema) - - val field_1 = - """{"name":"_1","type":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" - val field_2 = """{"name":"_2","type":"Simple"}""" - assert(result)(isRight(containsString(field_1) && containsString(field_2))) - } - ), - suite("primitives")( - test("encodes UnitType") { - val schema = Schema.primitive(StandardType.UnitType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"null\""))) - }, - test("encodes StringType") { - val schema = Schema.primitive(StandardType.StringType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"string\""))) - }, - test("encodes BooleanType") { - val schema = Schema.primitive(StandardType.BoolType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"boolean\""))) - }, - test("encodes ShortType") { - val schema = Schema.primitive(StandardType.ShortType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"short"}"""))) - }, - test("encodes IntType") { - val schema = Schema.primitive(StandardType.IntType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"int\""))) - }, - test("encodes LongType") { - val schema = Schema.primitive(StandardType.LongType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"long\""))) - }, - test("encodes FloatType") { - val schema = Schema.primitive(StandardType.FloatType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"float\""))) - }, - test("encodes DoubleType") { - val schema = Schema.primitive(StandardType.DoubleType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"double\""))) - }, - test("encodes BinaryType as bytes") { - val schema = Schema.primitive(StandardType.BinaryType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"bytes\""))) - }, - test("encodes BinaryType as fixed") { - val size = 12 - val schema = - Schema - .primitive(StandardType.BinaryType) - .annotate(AvroAnnotations.bytes(BytesType.Fixed(size, "MyFixed"))) - val result = AvroCodec.encode(schema) - - val expected = """{"type":"fixed","name":"MyFixed","doc":"","size":12}""" - assert(result)(isRight(equalTo(expected))) - }, - test("encodes CharType") { - val schema = Schema.primitive(StandardType.CharType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"char"}"""))) - }, - test("encodes UUIDType") { - val schema = Schema.primitive(StandardType.UUIDType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"string","logicalType":"uuid"}"""))) - }, - test("encodes BigDecimalType as Bytes") { - val schema = - Schema.primitive(StandardType.BigDecimalType).annotate(AvroAnnotations.decimal(DecimalType.Bytes)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":48,"scale":24}"""))) - }, - test("encodes BigDecimalType as Bytes with scala and precision") { - val schema = Schema - .primitive(StandardType.BigDecimalType) - .annotate(AvroAnnotations.decimal(DecimalType.Bytes)) - .annotate(AvroAnnotations.scale(10)) - .annotate(AvroAnnotations.precision(20)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":20,"scale":10}"""))) - }, - test("encodes BigDecimalType as Fixed") { - val schema = - Schema.primitive(StandardType.BigDecimalType).annotate(AvroAnnotations.decimal(DecimalType.Fixed(21))) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"fixed","name":"Decimal_48_24","size":21,"logicalType":"decimal","precision":48,"scale":24}""" - ) - ) - ) - }, - test("encodes BigDecimalType as Fixed with scala and precision") { - val schema = Schema - .primitive(StandardType.BigDecimalType) - .annotate(AvroAnnotations.decimal(DecimalType.Fixed(9))) - .annotate(AvroAnnotations.scale(10)) - .annotate(AvroAnnotations.precision(20)) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"fixed","name":"Decimal_20_10","size":9,"logicalType":"decimal","precision":20,"scale":10}""" - ) - ) - ) - }, - test("encodes BigIntegerType as Bytes") { - val schema = - Schema.primitive(StandardType.BigIntegerType).annotate(AvroAnnotations.decimal(DecimalType.Bytes)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":24,"scale":24}"""))) - }, - test("encodes BigIntegerType as Bytes with scala and precision") { - val schema = Schema - .primitive(StandardType.BigIntegerType) - .annotate(AvroAnnotations.decimal(DecimalType.Bytes)) - .annotate(AvroAnnotations.scale(10)) - .annotate(AvroAnnotations.precision(20)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":10,"scale":10}"""))) - }, - test("encodes BigIntegerType as Fixed") { - val schema = - Schema.primitive(StandardType.BigIntegerType).annotate(AvroAnnotations.decimal(DecimalType.Fixed(11))) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"fixed","name":"Decimal_24_24","size":11,"logicalType":"decimal","precision":24,"scale":24}""" - ) - ) - ) - }, - test("encodes BigIntegerType as Fixed with scala and precision") { - val schema = Schema - .primitive(StandardType.BigIntegerType) - .annotate(AvroAnnotations.decimal(DecimalType.Fixed(5))) - .annotate(AvroAnnotations.scale(10)) - .annotate(AvroAnnotations.precision(20)) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":10,"scale":10}""" - ) - ) - ) - }, - test("encodes DayOfWeekType") { - val schema = Schema.primitive(StandardType.DayOfWeekType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"dayOfWeek"}"""))) - }, - test("encodes MonthType") { - val schema = Schema.primitive(StandardType.MonthType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"month"}"""))) - }, - test("encodes YearType") { - val schema = Schema.primitive(StandardType.YearType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"year"}"""))) - }, - test("encodes ZoneIdType") { - val schema = Schema.primitive(StandardType.ZoneIdType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"string","zio.schema.codec.stringType":"zoneId"}"""))) - }, - test("encodes ZoneOffsetType") { - val schema = Schema.primitive(StandardType.ZoneOffsetType) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"zoneOffset"}"""))) - }, - //TODO 1 - //test("encodes MonthDayType") { - // val schema = Schema.primitive(StandardType.MonthDayType) - // val result = AvroCodec.encode(schema) - - // assert(result)( - // isRight( - // equalTo( - // """{"type":"record","name":"MonthDay","namespace":"zio.schema.codec.avro","fields":[{"name":"month","type":"int"},{"name":"day","type":"int"}],"zio.schema.codec.recordType":"monthDay"}""" - // ) - // ) - // ) - //}, - //TODO 2 - //test("encodes PeriodType") { - // val schema = Schema.primitive(StandardType.PeriodType) - // val result = AvroCodec.encode(schema) - // - // assert(result)( - // isRight( - // equalTo( - // """{"type":"record","name":"Period","namespace":"zio.schema.codec.avro","fields":[{"name":"years","type":"int"},{"name":"months","type":"int"},{"name":"days","type":"int"}],"zio.schema.codec.recordType":"period"}""" - // ) - // ) - // ) - //}, - //TODO 3 - //test("encodes YearMonthType") { - // val schema = Schema.primitive(StandardType.YearMonthType) - // val result = AvroCodec.encode(schema) - // - // assert(result)( - // isRight( - // equalTo( - // """{"type":"record","name":"YearMonth","namespace":"zio.schema.codec.avro","fields":[{"name":"year","type":"int"},{"name":"month","type":"int"}],"zio.schema.codec.recordType":"yearMonth"}""" - // ) - // ) - // ) - //}, - //TODO 4 - //test("encodes Duration") { - // val schema = Schema.primitive(StandardType.DurationType) // .duration(ChronoUnit.DAYS)) - // val result = AvroCodec.encode(schema) - // - // assert(result)( - // isRight( - // equalTo( - // """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration","zio.schema.codec.avro.durationChronoUnit":"DAYS"}""" - // ) - // ) - // ) - //}, - test("encodes InstantType as string") { - val schema = Schema.primitive(StandardType.InstantType).annotate(AvroAnnotations.formatToString) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"instant"}""" - ) - ) - ) - }, - test("encodes LocalDateType as string") { - val schema = - Schema.primitive(StandardType.LocalDateType).annotate(AvroAnnotations.formatToString) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"localDate"}""" - ) - ) - ) - }, - test("encodes LocalTimeType as string") { - val schema = - Schema.primitive(StandardType.LocalTimeType).annotate(AvroAnnotations.formatToString) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"localTime"}""" - ) - ) - ) - }, - test("encodes LocalDateTimeType as string") { - val schema = - Schema.primitive(StandardType.LocalDateTimeType).annotate(AvroAnnotations.formatToString) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"localDateTime"}""" - ) - ) - ) - }, - test("encodes OffsetTimeType") { - val schema = Schema.primitive(StandardType.OffsetTimeType) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"offsetTime"}""" - ) - ) - ) - }, - test("encodes OffsetDateTimeType") { - val schema = Schema.primitive(StandardType.OffsetDateTimeType) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"offsetDateTime"}""" - ) - ) - ) - }, - test("encodes ZonedDateTimeType") { - val schema = Schema.primitive(StandardType.ZonedDateTimeType) - val result = AvroCodec.encode(schema) - - assert(result)( - isRight( - equalTo( - """{"type":"string","zio.schema.codec.stringType":"zoneDateTime"}""" - ) - ) - ) - } - ), - test("fail should fail the encode") { - val schema = Schema.fail("I'm failing") - val result = AvroCodec.encode(schema) - - assert(result)(isLeft(equalTo("""I'm failing"""))) - }, - test("lazy is handled properly") { - val schema = Schema.Lazy(() => Schema.primitive(StandardType.StringType)) - val result = AvroCodec.encode(schema) - - assert(result)(isRight(equalTo("\"string\""))) - } - ), - /** - * Test Decoder - */ - suite("decode")( - suite("record")( - test("decode a simple record") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema.map(_.ast))( - isRight( - equalTo( - Schema - .record( - TypeId.fromTypeName("TestRecord"), - Schema.Field( - "s", - Schema.primitive(StandardType.StringType), - get0 = (p: ListMap[String, _]) => p("s").asInstanceOf[String], - set0 = (p: ListMap[String, _], v: String) => p.updated("s", v) - ), - Schema.Field( - "b", - Schema.primitive(StandardType.BoolType), - get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], - set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) - ) - ) - .ast - ) - ) - ) - }, - test("decode a nested record") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"nested","type":{"type":"record","name":"Inner","fields":[{"name":"innerS","type":"string"}]}},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - val expectedSchema = Schema.record( - TypeId.fromTypeName("TestRecord"), - Schema.Field( - "nested", - Schema.record( - TypeId.fromTypeName("Inner"), - Schema.Field( - "innerS", - Schema.primitive(StandardType.StringType), - get0 = (p: ListMap[String, _]) => p("innerS").asInstanceOf[String], - set0 = (p: ListMap[String, _], v: String) => p.updated("innerS", v) - ) - ), - get0 = (p: ListMap[String, _]) => p("nested").asInstanceOf[ListMap[String, _]], - set0 = (p: ListMap[String, _], v: ListMap[String, _]) => p.updated("nested", v) - ), - Schema.Field( - "b", - Schema.primitive(StandardType.BoolType), - get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], - set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) - ) - ) - - assert(schema.map(_.ast))(isRight(equalTo(expectedSchema.ast))) - }, - test("unwrap a wrapped initialSchemaDerived") { - val s = - """{"type":"record","zio.schema.codec.avro.wrapper":true,"name":"wrapper_xyz","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema.map(_.ast))( - isRight( - equalTo( - Schema - .record( - TypeId.fromTypeName("TestRecord"), - Schema.Field( - "s", - Schema.primitive(StandardType.StringType), - get0 = (p: ListMap[String, _]) => p("s").asInstanceOf[String], - set0 = (p: ListMap[String, _], v: String) => p.updated("s", v) - ), - Schema.Field( - "b", - Schema.primitive(StandardType.BoolType), - get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], - set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) - ) - ) - .ast - ) - ) - ) - }, - test("period record") { - val s = - """{"type":"record","name":"Period","namespace":"zio.schema.codec.avro","fields":[{"name":"years","type":"int"},{"name":"months","type":"int"},{"name":"days","type":"int"}],"zio.schema.codec.recordType":"period"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.PeriodType))) - }, - test("yearMonth record") { - val s = - """{"type":"record","name":"YearMonth","namespace":"zio.schema.codec.avro","fields":[{"name":"year","type":"int"},{"name":"month","type":"int"}],"zio.schema.codec.recordType":"yearMonth"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.YearMonthType))) - }, - test("tuple record successful") { - val s = - """{"type":"record","name":"Tuple","namespace":"zio.schema.codec.avro","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"int"}],"zio.schema.codec.recordType":"tuple"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isTuple(isStandardType(StandardType.StringType), isStandardType(StandardType.IntType))) - ) - }, - test("tuple record failing") { - val s = - """{"type":"record","name":"Tuple","namespace":"zio.schema.codec.avro","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"int"},{"name":"_3","type":"int"}],"zio.schema.codec.recordType":"tuple"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isLeft) - }, - test("monthDay record") { - val s = - """{"type":"record","name":"MonthDay","namespace":"zio.schema.codec.avro","fields":[{"name":"month","type":"int"},{"name":"day","type":"int"}],"zio.schema.codec.recordType":"monthDay"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.MonthDayType))) - }, - test("duration record without chrono unit annotation") { - val s = - """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.DurationType))) //(ChronoUnit.MILLIS)))) - }, - test("duration record chrono unit annotation") { - val s = - """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration","zio.schema.codec.avro.durationChronoUnit":"DAYS"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.DurationType))) //(ChronoUnit.DAYS)))) - }, - test("assign the name annotation") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasNameAnnotation(equalTo("TestRecord"))))) - }, - test("assign the namespace annotation") { - val s = - """{"type":"record","name":"TestRecord","namespace":"MyTest","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasNamespaceAnnotation(equalTo("MyTest"))))) - }, - test("not assign the namespace annotation if missing") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasNamespaceAnnotation(anything).negate))) - }, - zio.test.test("assign the doc annotation") { - val s = - """{"type":"record","name":"TestRecord","doc":"Very helpful documentation!","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasDocAnnotation(equalTo("Very helpful documentation!"))))) - }, - test("not assign the doc annotation if missing") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasDocAnnotation(anything).negate))) - }, - test("assign the aliases annotation") { - val s = - """{"type":"record","name":"TestRecord","aliases":["wow", "cool"],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isRecord(hasAliasesAnnotation(exists[String](equalTo("wow")) && exists(equalTo("cool"))))) - ) - }, - test("not assign the aliases annotation if missing") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasAliasesAnnotation(anything).negate))) - }, - test("not assign the aliases annotation if empty") { - val s = - """{"type":"record","name":"TestRecord","aliases":[],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasAliasesAnnotation(anything).negate))) - }, - zio.test.test("assign the error annotation") { - val s = - """{"type":"error","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasErrorAnnotation))) - }, - test("not assign the error annotation if not an error") { - val s = - """{"type":"record","name":"TestRecord","aliases":[],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isRecord(hasErrorAnnotation.negate))) - } - ), - suite("fields")( - test("decodes primitive fields of record") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField(hasLabel(equalTo("s")) && hasSchema(isStandardType(StandardType.StringType))) - val field2 = hasRecordField(hasLabel(equalTo("b")) && hasSchema(isStandardType(StandardType.BoolType))) - assert(schema)(isRight(isRecord(field1 && field2))) - }, - test("decodes the fields complex initialSchemaDerived") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"complex","type":{"type":"record","name":"Complex","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField(hasLabel(equalTo("s")) && hasSchema(isStandardType(StandardType.StringType))) - val field2 = hasRecordField(hasLabel(equalTo("b")) && hasSchema(isStandardType(StandardType.BoolType))) - val complex = isRecord(field1 && field2) - val field = hasRecordField(hasLabel(equalTo("complex")) && hasSchema(complex)) - assert(schema)(isRight(isRecord(field))) - }, - zio.test.test("assign the field name annotation") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField(hasLabel(equalTo("s")) && hasNameAnnotation(equalTo("s"))) - val field2 = hasRecordField(hasLabel(equalTo("b")) && hasNameAnnotation(equalTo("b"))) - assert(schema)(isRight(isRecord(field1 && field2))) - }, - zio.test.test("assign the field doc annotation iff it exists") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","doc":"Very helpful doc!","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField(hasLabel(equalTo("s")) && hasDocAnnotation(equalTo("Very helpful doc!"))) - val field2 = hasRecordField(hasLabel(equalTo("b")) && hasDocAnnotation(anything).negate) - assert(schema)(isRight(isRecord(field1 && field2))) - }, - test("assign the field default annotation") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","default":"defaultValue","type":"string"},{"name":"complex","default":{"s":"defaultS","b":true},"type":{"type":"record","name":"Complex","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField(hasLabel(equalTo("s")) && hasFieldDefaultAnnotation(equalTo("defaultValue"))) - val field2 = hasRecordField( - hasLabel(equalTo("complex")) && hasFieldDefaultAnnotation(asString(equalTo("""{s=defaultS, b=true}"""))) - ) - val field3 = hasRecordField(hasLabel(equalTo("b")) && hasFieldDefaultAnnotation(anything).negate) - assert(schema)(isRight(isRecord(field1 && field2 && field3))) - }, - zio.test.test("assign the fieldOrder annotation") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","order":"descending","type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField( - hasLabel(equalTo("s")) && hasFieldOrderAnnotation(equalTo(AvroAnnotations.FieldOrderType.Descending)) - ) - val field2 = hasRecordField( - hasLabel(equalTo("b")) && hasFieldOrderAnnotation(equalTo(AvroAnnotations.FieldOrderType.Ascending)) - ) - assert(schema)(isRight(isRecord(field1 && field2))) - }, - zio.test.test("assign the field aliases annotation") { - val s = - """{"type":"record","name":"TestRecord","fields":[{"name":"s","aliases":["wow", "cool"],"type":"string"},{"name":"b","type":"boolean"}]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val field1 = hasRecordField( - hasLabel(equalTo("s")) && hasAliasesAnnotation(Assertion.hasSameElements(Seq("wow", "cool"))) - ) - val field2 = hasRecordField(hasLabel(equalTo("b")) && hasAliasesAnnotation(anything).negate) - assert(schema)(isRight(isRecord(field1 && field2))) - } - ), - suite("enum")( - test("decodes symbols as union of strings") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val symbolKeysAssetion = Assertion.hasKeys(hasSameElements(Seq("a", "b", "c"))) - val enumStringTypeAssertion: Assertion[ListMap[String, (Schema[_], Chunk[Any])]] = - Assertion.hasValues(forall(tuple2First(isStandardType(StandardType.StringType)))) - assert(schema)(isRight(isEnum(enumStructure(symbolKeysAssetion && enumStringTypeAssertion)))) - }, - test("assign the enum name annotation") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasNameAnnotation(equalTo("TestEnum"))))) - }, - test("assign the enum namespace annotation") { - val s = """{"type":"enum","name":"TestEnum","namespace":"MyTest","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasNamespaceAnnotation(equalTo("MyTest"))))) - }, - test("not assign the enum namespace annotation if empty") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasNamespaceAnnotation(anything).negate))) - }, - test("assign the enum aliases annotation") { - val s = """{"type":"enum","name":"TestEnum","aliases":["MyAlias", "MyAlias2"],"symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasAliasesAnnotation(hasSameElements(Seq("MyAlias", "MyAlias2")))))) - }, - test("not assign the enum aliases annotation if empty") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasAliasesAnnotation(anything).negate))) - }, - test("assign the enum doc annotation") { - val s = - """{"type":"enum","name":"TestEnum","doc":"Some very helpful documentation!","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasDocAnnotation(equalTo("Some very helpful documentation!"))))) - }, - test("not assign the enum doc annotation if empty") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasAliasesAnnotation(anything).negate))) - }, - test("assign the enum default annotation") { - val s = """{"type":"enum","name":"TestEnum","default":"a","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasDefaultAnnotation(equalTo("a"))))) - }, - test("fail if enum default is not a symbol") { - val s = """{"type":"enum","name":"TestEnum","default":"d","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isLeft(equalTo("The Enum Default: d is not in the enum symbol set: [a, b, c]"))) - }, - test("not assign the enum default annotation if empty") { - val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isEnum(hasDefaultAnnotation(anything).negate))) - } - ), - test("decodes primitive array") { - val s = """{"type":"array","items":{"type":"int"}}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isSequence(hasSequenceElementSchema(isStandardType(StandardType.IntType))))) - }, - test("decodes complex array") { - val s = - """{"type":"array","items":{"type":"record","name":"TestRecord","fields":[{"name":"f1","type":"int"},{"name":"f2","type":"string"}]}}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - assert(schema)(isRight(isSequence(hasSequenceElementSchema(isRecord(anything))))) - }, - test("decodes map with string keys") { - val s = """{"type":"map","values":{"type":"int"}}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) + final case class Person(name: String, age: Int) - assert(schema)( - isRight( - isMap( - hasMapKeys(isStandardType(StandardType.StringType)) && hasMapValues( - isStandardType(StandardType.IntType) - ) - ) - ) - ) - }, - suite("union")( - test("option union with null on first position") { - val s = """[{"type":"null"}, {"type":"int"}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isOption(hasOptionElementSchema(isStandardType(StandardType.IntType))))) - }, - test("option union with null on second position") { - val s = """[{"type":"int"}, {"type":"null"}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isOption(hasOptionElementSchema(isStandardType(StandardType.IntType))))) - }, - test("not an option union with more than one element type") { - val s = """[{"type":"null"}, {"type":"int"}, {"type":"string"}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isOption(anything).negate)) - }, - test("nested either union") { - val s = - """{"type":"record","name":"wrapper_hashed_2071802344","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n465006219","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true},"string"]}],"zio.schema.codec.avro.either":true}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight( - isEither( - isEither(isOption(anything), isStandardType(StandardType.StringType)), - isStandardType(StandardType.StringType) - ) - ) - ) - }, - test("union as zio initialSchemaDerived enumeration") { - val s = """[{"type":"null"}, {"type":"int"}, {"type":"string"}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val assertion1 = hasKey("null", tuple2First(isStandardType(StandardType.UnitType))) - val sssertion2 = hasKey("int", tuple2First(isStandardType(StandardType.IntType))) - val assertion3 = hasKey("string", tuple2First(isStandardType(StandardType.StringType))) - assert(schema)(isRight(isEnum(enumStructure(assertion1 && sssertion2 && assertion3)))) - }, - test("correct case codec for case object of ADT") { - val s = - """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val assertionA = hasKey("A", tuple2First(isEmptyRecord)) - val assertionB = hasKey("B", tuple2First(isEmptyRecord)) - val assertionMyC = hasKey("MyC", tuple2First(isEmptyRecord)) - assert(schema)(isRight(isEnum(enumStructure(assertionA && assertionB && assertionMyC)))) - }, - test("correct case codec for case class of ADT") { - val s = - """[{"type":"record","name":"A","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val assertionA = hasKey( - "A", - tuple2First(isRecord(hasRecordField(hasLabel(equalTo("s"))) && hasRecordField(hasLabel(equalTo("b"))))) - ) - val assertionB = hasKey("B", tuple2First(isEmptyRecord)) - val assertionMyC = hasKey("MyC", tuple2First(isEmptyRecord)) - assert(schema)(isRight(isEnum(enumStructure(assertionA && assertionB && assertionMyC)))) - }, - test("unwrap nested union") { - val s = - """[{"type":"record","name":"wrapper_hashed_n465006219","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true},{"type":"record","name":"C","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val nestedEnumAssertion = isEnum( - enumStructure( - hasKey("A", tuple2First(isEmptyRecord)) && hasKey( - "B", - tuple2First(isEmptyRecord) - ) - ) - ) - val nestedEnumKey = - hasKey("zio.schema.codec.avro.wrapper_hashed_n465006219", tuple2First(nestedEnumAssertion)) - val cEnumKey = hasKey("C", tuple2First(isEmptyRecord)) - val dEnumKey = hasKey("D", tuple2First(isRecord(hasRecordField(hasLabel(equalTo("s")))))) - assert(schema)(isRight(isEnum(enumStructure(nestedEnumKey && cEnumKey && dEnumKey)))) - } - ), - suite("fixed")( - test("logical type decimal as BigDecimal") { - val s = - """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":11,"scale":10}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val isDecimalAssertion = isStandardType(StandardType.BigDecimalType) - val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = - exists(equalTo(AvroAnnotations.decimal(DecimalType.Fixed(5)))) - val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) - val hasPrecisionAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.precision(11))) - val hasAnnotationsAssertion = - annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && hasPrecisionAnnotation) - assert(schema)(isRight(isDecimalAssertion && hasAnnotationsAssertion)) - }, - test("logical type decimal as BigInteger") { - val s = - """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":10,"scale":10}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val isBigIntegerType = isStandardType(StandardType.BigIntegerType) - val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = - exists(equalTo(AvroAnnotations.decimal(DecimalType.Fixed(5)))) - val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) - val doesNotHavePrecisionAnnotation: Assertion[Iterable[Any]] = - exists(Assertion.isSubtype[AvroAnnotations.precision.type](anything)).negate - val hasAnnotationsAssertion = - annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && doesNotHavePrecisionAnnotation) - assert(schema)(isRight(isBigIntegerType && hasAnnotationsAssertion)) - }, - test("fail on invalid logical type") { - val s = - """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":9,"scale":10}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isLeft(equalTo("Invalid decimal scale: 10 (greater than precision: 9)"))) - }, - test("decode as binary") { - val s = """{"type":"fixed","name":"Something","size":5}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val hasNameAnnotation = annotations(exists(equalTo(AvroAnnotations.name("Something")))) - assert(schema)(isRight(isStandardType(StandardType.BinaryType) && hasNameAnnotation)) - } - ), - suite("string")( - test("decodes zoneId with formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"zoneId"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.ZoneIdType))) - }, - test("decodes instant with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"ISO_INSTANT"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes instant using default") { - val s = """{"type":"string","zio.schema.codec.stringType":"instant"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes instant with formatter pattern") { - val pattern = "yyyy MM dd" - val s = - s"""{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"$pattern"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decode DateTimeFormatter field fails on invalid formatter") { - val pattern = "this is not a valid formatter pattern" - val s = - s"""{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"$pattern"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isLeft(equalTo("Unknown pattern letter: t"))) - }, - test("decodes localDate with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"localDate","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) - }, - test("decodes localDate with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"localDate"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) - }, - test("decodes localTime with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"localTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes localTime with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"localTime"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes localDateTime with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"localDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) - }, - test("decodes localDateTime with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"localDateTime"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isStandardType(StandardType.LocalDateTimeType)) - ) - }, - test("decodes zonedDateTime with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"zoneDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.ZonedDateTimeType))) - }, - test("decodes zonedDateTime with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"zoneDateTime"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isStandardType(StandardType.ZonedDateTimeType)) - ) - }, - test("decodes offsetTime with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"offsetTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.OffsetTimeType))) - }, - test("decodes offsetTime with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"offsetTime"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.OffsetTimeType))) - }, - test("decodes offsetDateTime with formatter") { - val s = - """{"type":"string","zio.schema.codec.stringType":"offsetDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.OffsetDateTimeType))) - }, - test("decodes offsetDateTime with default formatter") { - val s = """{"type":"string","zio.schema.codec.stringType":"offsetDateTime"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isStandardType(StandardType.OffsetDateTimeType)) - ) - }, - test("decodes logical type uuid") { - val s = """{"type":"string","logicalType":"uuid"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.UUIDType))) - }, - test("decodes primitive type string") { - val s = """{"type":"string"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.StringType))) - } - ), - suite("bytes")( - test("logical type decimal as BigDecimal") { - val s = """{"type":"bytes","logicalType":"decimal","precision":20,"scale":10}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val isDecimalAssertion = isStandardType(StandardType.BigDecimalType) - val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = - exists(equalTo(AvroAnnotations.decimal(DecimalType.Bytes))) - val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) - val hasPrecisionAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.precision(20))) - val hasAnnotationsAssertion = - annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && hasPrecisionAnnotation) - assert(schema)(isRight(isDecimalAssertion && hasAnnotationsAssertion)) - }, - test("logical type decimal as BigInteger") { - val s = """{"type":"bytes","logicalType":"decimal","precision":20,"scale":20}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - val isBigIntegerAssertion = isStandardType(StandardType.BigIntegerType) - val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = - exists(equalTo(AvroAnnotations.decimal(DecimalType.Bytes))) - val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(20))) - val hasAnnotationsAssertion = annotations(hasDecimalTypeAnnotation && hasScalaAnnotation) - assert(schema)(isRight(isBigIntegerAssertion && hasAnnotationsAssertion)) - }, - test("decode as binary") { - val s = """{"type":"bytes"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.BinaryType))) - } - ), - suite("int")( - test("decodes char") { - val s = """{"type":"int","zio.schema.codec.intType":"char"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.CharType))) - }, - test("decodes dayOfWeek") { - val s = """{"type":"int","zio.schema.codec.intType":"dayOfWeek"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.DayOfWeekType))) - }, - test("decodes Year") { - val s = """{"type":"int","zio.schema.codec.intType":"year"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.YearType))) - }, - test("decodes short") { - val s = """{"type":"int","zio.schema.codec.intType":"short"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.ShortType))) - }, - test("decodes month") { - val s = """{"type":"int","zio.schema.codec.intType":"month"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.MonthType))) - }, - test("decodes zoneOffset") { - val s = """{"type":"int","zio.schema.codec.intType":"zoneOffset"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.ZoneOffsetType))) - }, - test("decodes int") { - val s = """{"type":"int"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.IntType))) - }, - test("decodes logical type timemillis") { - val s = - """{"type":"int","logicalType":"time-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes logical type timemillis with default formatter") { - val s = """{"type":"int","logicalType":"time-millis"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes logical type date") { - val s = - """{"type":"int","logicalType":"date"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) - }, - test("decodes logical type date with default formatter") { - val s = """{"type":"int","logicalType":"date"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) - } - ), - suite("long")( - test("decodes long") { - val s = """{"type":"long"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LongType))) - }, - test("decodes logical type timeMicros") { - val s = - """{"type":"long","logicalType":"time-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes logical type timeMicros with default formatter") { - val s = """{"type":"long","logicalType":"time-micros"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) - }, - test("decodes logical type timestampMillis") { - val s = - """{"type":"long","logicalType":"timestamp-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes logical type timestampMillis with default formatter") { - val s = """{"type":"long","logicalType":"timestamp-millis"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes logical type timestampMicros") { - val s = - """{"type":"long","logicalType":"timestamp-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes logical type timestampMicros with default formatter") { - val s = """{"type":"long","logicalType":"timestamp-micros"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.InstantType))) - }, - test("decodes logical type LocalTimestamp millis") { - val s = - """{"type":"long","logicalType":"local-timestamp-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) - }, - test("decodes logical type LocalTimestamp millis with default formatter") { - val s = """{"type":"long","logicalType":"local-timestamp-millis"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isStandardType(StandardType.LocalDateTimeType)) - ) - }, - test("decodes logical type LocalTimestamp micros") { - val s = - """{"type":"long","logicalType":"local-timestamp-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) - }, - test("decodes logical type LocalTimestamp micros with default formatter") { - val s = """{"type":"long","logicalType":"local-timestamp-micros"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) - - assert(schema)( - isRight(isStandardType(StandardType.LocalDateTimeType)) - ) - } - ), - test("float") { - val s = """{"type":"float"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) + object Person { + implicit lazy val schema: Schema[Person] = DeriveSchema.gen[Person] + } - assert(schema)(isRight(isStandardType(StandardType.FloatType))) - }, - test("double") { - val s = """{"type":"double"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) + case class Record(name: String, value: Int) - assert(schema)(isRight(isStandardType(StandardType.DoubleType))) - }, - test("boolean") { - val s = """{"type":"boolean"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) + object Record { + implicit val schemaRecord: Schema[Record] = DeriveSchema.gen[Record] + } - assert(schema)(isRight(isStandardType(StandardType.BoolType))) - }, - test("null") { - val s = """{"type":"null"}""" - val schema = AvroCodec.decode(Chunk.fromArray(s.getBytes())) + case class Class0() - assert(schema)(isRight(isStandardType(StandardType.UnitType))) - } - ), - test("encode/decode full adt test") { - val initialSchemaDerived = DeriveSchema.gen[FullAdtTest.TopLevelUnion] + object Class0 { + implicit val schemaClass0: Schema[Class0] = DeriveSchema.gen[Class0] + } - val decoded = for { - avroSchemaString <- AvroCodec.encode(initialSchemaDerived) - decoded <- AvroCodec.decode(Chunk.fromArray(avroSchemaString.getBytes())) - //_ <- AvroCodec.encode(decoded) TODO: this fails - } yield decoded + case class Class1(value: Int) - assert(decoded)(isRight(hasField("ast", _.ast, equalTo(initialSchemaDerived.ast)))) - } @@ TestAspect.ignore // TODO: FIX - ) -} + object Class1 { + implicit val schemaClass0: Schema[Class1] = DeriveSchema.gen[Class1] + } -object AssertionHelper { + case class Composer(name: String, birthplace: String, compositions: List[String]) - def isRecord[A](assertion: Assertion[Schema.Record[A]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Record[A]]( - "Record", { - case r: Schema.Record[_] => Try { r.asInstanceOf[Schema.Record[A]] }.toOption - case _ => None - }, - assertion - ) + object Composer { + implicit val schemaComposer: Schema[Composer] = DeriveSchema.gen[Composer] + } - def isEmptyRecord[A]: Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema[_]]( - "EmptyRecord", { - case r: CaseClass0[_] => Some(r) - case r @ GenericRecord(_, structure, _) if structure.toChunk.isEmpty => Some(r) - case _ => None - }, - anything - ) + case class Ingredient(name: String, sugar: Double, fat: Double) + case class Pizza(name: String, ingredients: List[Ingredient], vegetarian: Boolean, vegan: Boolean, calories: Int) - def isEnum[A](assertion: Assertion[Schema.Enum[A]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Enum[A]]( - "Enum", { - case r: Schema.Enum[_] => Try { r.asInstanceOf[Schema.Enum[A]] }.toOption - case _ => None - }, - assertion - ) + object Pizza { + implicit val schemaIngredient: Schema[Ingredient] = DeriveSchema.gen[Ingredient] + implicit val schemaPizza: Schema[Pizza] = DeriveSchema.gen[Pizza] + } - def isSequence[A](assertion: Assertion[Schema.Sequence[_, A, _]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Sequence[_, A, _]]( - "List", { - case r: Schema.Sequence[_, _, _] => Try { r.asInstanceOf[Schema.Sequence[_, A, _]] }.toOption - case _ => None - }, - assertion - ) + case class HighArity( + f1: Int = 1, + f2: Int = 2, + f3: Int = 3, + f4: Int = 4, + f5: Int = 5, + f6: Int = 6, + f7: Int = 7, + f8: Int = 8, + f9: Int = 9, + f10: Int = 10, + f11: Int = 11, + f12: Int = 12, + f13: Int = 13, + f14: Int = 14, + f15: Int = 15, + f16: Int = 16, + f17: Int = 17, + f18: Int = 18, + f19: Int = 19, + f20: Int = 20, + f21: Int = 21, + f22: Int = 22, + f23: Int = 23, + f24: Int = 24 + ) + + object HighArity { + implicit val schemaHighArityRecord: Schema[HighArity] = DeriveSchema.gen[HighArity] + } - def isMap[K, V](assertion: Assertion[Schema.Map[K, V]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Map[K, V]]( - "Map", { - case r: Schema.Map[_, _] => Try { r.asInstanceOf[Schema.Map[K, V]] }.toOption - case _ => None - }, - assertion - ) + sealed trait OneOf - def isTuple[A, B](assertion: Assertion[Schema.Tuple2[A, B]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Tuple2[A, B]]( - "Tuple", { - case r: Schema.Tuple2[_, _] => Try { r.asInstanceOf[Schema.Tuple2[A, B]] }.toOption - case _ => None - }, - assertion - ) + object OneOf { + case class StringValue(value: String) extends OneOf - def isTuple[A, B](assertionA: Assertion[Schema[A]], assertionB: Assertion[Schema[B]]): Assertion[Schema[_]] = - isTuple[A, B]( - hasField[Schema.Tuple2[A, B], Schema[A]]("left", _.left, assertionA) && hasField[Schema.Tuple2[A, B], Schema[B]]( - "right", - _.right, - assertionB - ) - ) + case class IntValue(value: Int) extends OneOf - def isEither[A, B](assertion: Assertion[Schema.Either[A, B]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Either[A, B]]( - "Either", { - case r: Schema.Either[_, _] => Try { r.asInstanceOf[Schema.Either[A, B]] }.toOption - case _ => None - }, - assertion - ) + case class BooleanValue(value: Boolean) extends OneOf - def isEither[A, B](leftAssertion: Assertion[Schema[A]], rightAssertion: Assertion[Schema[B]]): Assertion[Schema[_]] = - isEither[A, B]( - hasField[Schema.Either[A, B], Schema[A]]("left", _.left, leftAssertion) && hasField[ - Schema.Either[A, B], - Schema[B] - ]("right", _.right, rightAssertion) - ) + case object NullValue extends OneOf - def isOption[A](assertion: Assertion[Schema.Optional[A]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Schema.Optional[A]]( - "Optional", { - case r: Schema.Optional[_] => Try { r.asInstanceOf[Schema.Optional[A]] }.toOption - case _ => None - }, - assertion - ) + implicit val schemaOneOf: Schema[OneOf] = DeriveSchema.gen[OneOf] + } - def tuple2First[A](assertion: Assertion[A]): Assertion[(A, _)] = - Assertion.isCase[(A, _), A]("Tuple", { - case (a, _) => Some(a) - }, assertion) + @avroEnum() sealed trait Enums - def hasMapKeys[K](assertion: Assertion[Schema[K]]): Assertion[Schema.Map[K, _]] = - hasField("keySchema", _.keySchema, assertion) + object Enums { + case object A extends Enums + case object B extends Enums + case object C extends Enums + case object D extends Enums - def hasMapValues[V](assertion: Assertion[Schema[V]]): Assertion[Schema.Map[_, V]] = - hasField("valueSchema", _.valueSchema, assertion) + case object E extends Enums - def enumStructure(assertion: Assertion[ListMap[String, (Schema[_], Chunk[Any])]]): Assertion[Schema.Enum[_]] = - Assertion.assertionRec("enumStructure")(assertion)( - enum => - Some(`enum`.cases.foldRight(ListMap.empty[String, (Schema[_], Chunk[Any])]) { (caseValue, acc) => - (acc + (caseValue.id -> scala.Tuple2(caseValue.schema, caseValue.annotations))) - }) - ) + implicit val schemaEnums: Schema[Enums] = DeriveSchema.gen[Enums] + } - def annotations(assertion: Assertion[Chunk[Any]]): Assertion[Any] = - Assertion.assertionRec("hasAnnotations")(assertion) { - case s: Schema[_] => Some(s.annotations) - case f: Schema.Field[_, _] => Some(f.annotations) - case _ => None + override def spec: Spec[TestEnvironment with Scope, Any] = suite("Avro Codec Spec")( + primitiveEncoderSpec, + collectionsEncoderSpec, + optionEncoderSpec, + eitherEncoderSpec, + tupleEncoderSpec, + genericRecordEncoderSpec, + caseClassEncoderSpec, + enumEncoderSpec, + primitiveDecoderSpec, + optionDecoderSpec, + eitherDecoderSpec, + tupleDecoderSpec, + sequenceDecoderSpec, + genericRecordDecoderSpec, + enumDecoderSpec, + streamEncodingDecodingSpec, + genericRecordEncodeDecodeSpec + ) + + private val primitiveEncoderSpec = suite("Avro Codec - Encoder primitive spec")( + test("Encode string") { + val codec = AvroCodec.schemaBasedBinaryCodec[String] + val bytes = codec.encode("Hello") + assertTrue(bytes.length == 6) + }, + test("Encode Int") { + val codec = AvroCodec.schemaBasedBinaryCodec[Int] + val bytes = codec.encode(42) + assertTrue(bytes.length == 1) + }, + test("Encode Short") { + val codec = AvroCodec.schemaBasedBinaryCodec[Short] + val bytes = codec.encode(42) + assertTrue(bytes.length == 1) + }, + test("Encode Long") { + val codec = AvroCodec.schemaBasedBinaryCodec[Long] + val bytes = codec.encode(600000000000L) + assertTrue(bytes.length == 6) + }, + test("Encode Float") { + val codec = AvroCodec.schemaBasedBinaryCodec[Float] + val bytes = codec.encode(42.0f) + assertTrue(bytes.length == 4) + }, + test("Encode Double") { + val codec = AvroCodec.schemaBasedBinaryCodec[Double] + val bytes = codec.encode(42.0) + assertTrue(bytes.length == 8) + }, + test("Encode Boolean") { + val codec = AvroCodec.schemaBasedBinaryCodec[Boolean] + val bytes = codec.encode(true) + assertTrue(bytes.length == 1) + }, + test("Encode Char") { + val codec = AvroCodec.schemaBasedBinaryCodec[Char] + val bytes = codec.encode('a') + assertTrue(bytes.length == 2) + }, + test("Encode UUID") { + val codec = AvroCodec.schemaBasedBinaryCodec[UUID] + val bytes = codec.encode(UUID.randomUUID()) + assertTrue(bytes.length == 37) + }, + test("Encode BigDecimal") { + val codec = AvroCodec.schemaBasedBinaryCodec[BigDecimal] + val bytes = codec.encode(BigDecimal.valueOf(42.0)) + assertTrue(bytes.length == 12) + }, + test("Encode BigDecimal") { + val codec = AvroCodec.schemaBasedBinaryCodec[BigInteger] + val bytes = codec.encode(BigInteger.valueOf(42)) + assertTrue(bytes.length == 12) + }, + test("Encode DayOfWeek") { + val codec = AvroCodec.schemaBasedBinaryCodec[DayOfWeek] + val bytes = codec.encode(DayOfWeek.FRIDAY) + assertTrue(bytes.length == 1) + }, + test("Encode Month") { + val codec = AvroCodec.schemaBasedBinaryCodec[Month] + val bytes = codec.encode(Month.APRIL) + assertTrue(bytes.length == 1) + }, + test("Encode MonthDay") { + val codec = AvroCodec.schemaBasedBinaryCodec[MonthDay] + val bytes = codec.encode(MonthDay.of(4, 1)) + assertTrue(bytes.length == 8) + }, + test("Encode Period") { + val codec = AvroCodec.schemaBasedBinaryCodec[Period] + val bytes = codec.encode(Period.ofDays(4)) + assertTrue(bytes.length == 4) + }, + test("Encode Year") { + val codec = AvroCodec.schemaBasedBinaryCodec[Year] + val bytes = codec.encode(Year.of(2021)) + assertTrue(bytes.length == 2) + }, + test("Encode YearMonth") { + val codec = AvroCodec.schemaBasedBinaryCodec[YearMonth] + val bytes = codec.encode(YearMonth.of(2021, 4)) + assertTrue(bytes.length == 8) + }, + test("Encode ZoneOffset") { + val codec = AvroCodec.schemaBasedBinaryCodec[ZoneOffset] + val bytes = codec.encode(ZoneOffset.MAX) + assertTrue(bytes.length == 3) + }, + test("Encode Unit") { + val codec = AvroCodec.schemaBasedBinaryCodec[Unit] + val bytes = codec.encode(()) + assertTrue(bytes.isEmpty) } - - def hasNameAnnotation(assertion: Assertion[String]): Assertion[Any] = - annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.name](hasField("name", _.name, assertion)))) - - def hasNamespaceAnnotation(assertion: Assertion[String]): Assertion[Any] = - annotations( - Assertion.exists(Assertion.isSubtype[AvroAnnotations.namespace](hasField("namespace", _.namespace, assertion))) - ) - - def hasDocAnnotation(assertion: Assertion[String]): Assertion[Any] = - annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.doc](hasField("doc", _.doc, assertion)))) - - def hasFieldOrderAnnotation(assertion: Assertion[FieldOrderType]): Assertion[Field[_, _]] = - annotations( - Assertion.exists( - Assertion.isSubtype[AvroAnnotations.fieldOrder](hasField("fieldOrderType", _.fieldOrderType, assertion)) - ) - ) - - def hasAliasesAnnotation(assertion: Assertion[Iterable[String]]): Assertion[Any] = - annotations( - Assertion.exists(Assertion.isSubtype[AvroAnnotations.aliases](hasField("aliases", _.aliases, assertion))) - ) - - def hasFieldDefaultAnnotation(assertion: Assertion[Object]): Assertion[Field[_, _]] = - annotations( - Assertion.exists( - Assertion.isSubtype[AvroAnnotations.default](hasField("javaDefaultObject", _.javaDefaultObject, assertion)) - ) - ) - - def hasDefaultAnnotation(assertion: Assertion[Object]): Assertion[Schema[_]] = - annotations( - Assertion.exists( - Assertion.isSubtype[AvroAnnotations.default](hasField("javaDefaultObject", _.javaDefaultObject, assertion)) - ) - ) - - val hasErrorAnnotation: Assertion[Any] = - annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.error.type](Assertion.anything))) - - def asString(assertion: Assertion[String]): Assertion[Any] = - Assertion.assertionRec("asString")(assertion)(v => Some(v.toString)) - - def recordFields(assertion: Assertion[Iterable[Schema.Field[_, _]]]): Assertion[Schema.Record[_]] = - Assertion.assertionRec[Schema.Record[_], Chunk[Field[_, _]]]("hasRecordField")( - assertion - ) { - case r: Schema.Record[_] => Some(r.fields) - case _ => None + ) + + private val collectionsEncoderSpec = suite("Avro Codec - Encoder Collection spec")( + test("Encode Chunk[Byte]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[Byte]] + val bytes = codec.encode(Chunk.fromArray(Array[Byte](1, 2, 3))) + assertTrue(bytes.length == 5) + }, + test("Encode Chunk[String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[String]] + val bytes = codec.encode(Chunk.fromArray(Array[String]("John", "Adam", "Daniel"))) + assertTrue(bytes.length == 19) + }, + test("Encode List[String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[List[String]] + val bytes = codec.encode(List("John", "Adam", "Daniel")) + assertTrue(bytes.length == 19) + }, + test("Encode Set[String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Set[String]] + val bytes = codec.encode(Set("John", "Adam", "Daniel")) + assertTrue(bytes.length == 19) + }, + test("Encode Map[String, String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Map[String, String]] + val bytes = codec.encode(Map("Name" -> "John", "Name" -> "Adam", "Name" -> "Daniel")) + assertTrue(bytes.length == 14) } - - def hasSequenceElementSchema[A](assertion: Assertion[Schema[A]]): Assertion[Schema.Sequence[_, A, _]] = - Assertion.hasField("schemaA", _.elementSchema, assertion) - - def hasOptionElementSchema[A](assertion: Assertion[Schema[A]]): Assertion[Schema.Optional[A]] = - Assertion.hasField("schema", _.schema, assertion) - - def hasRecordField(assertion: Assertion[Schema.Field[_, _]]): Assertion[Schema.Record[_]] = - recordFields(Assertion.exists(assertion)) - - def hasLabel(assertion: Assertion[String]): Assertion[Schema.Field[_, _]] = - hasField("label", _.name, assertion) - - def hasSchema(assertion: Assertion[Schema[_]]): Assertion[Schema.Field[_, _]] = - hasField("initialSchemaDerived", _.schema, assertion) - - def isPrimitive[A](assertion: Assertion[Primitive[A]]): Assertion[Schema[_]] = - Assertion.isCase[Schema[_], Primitive[A]]("Primitive", { - case p: Primitive[_] => Try { p.asInstanceOf[Primitive[A]] }.toOption - case _ => None - }, assertion) - - def isStandardType[A](standardType: StandardType[A]): Assertion[Schema[_]] = - isPrimitive[A](hasField("standardType", _.standardType, equalTo(standardType))) - - def isPrimitiveType[A](assertion: Assertion[StandardType[A]]): Assertion[Schema[_]] = - isPrimitive[A](hasField("standardType", _.standardType, assertion)) -} - -object SpecTestData { - - @AvroAnnotations.name("MyEnum") - sealed trait CaseObjectsOnlyAdt - - object CaseObjectsOnlyAdt { - case object A extends CaseObjectsOnlyAdt - case object B extends CaseObjectsOnlyAdt - - @AvroAnnotations.name("MyC") - case object C extends CaseObjectsOnlyAdt - } - - @AvroAnnotations.avroEnum - sealed trait CaseObjectAndCaseClassAdt - - object CaseObjectAndCaseClassAdt { - case object A extends CaseObjectAndCaseClassAdt - case object B extends CaseObjectAndCaseClassAdt - - @AvroAnnotations.name("MyC") - case object C extends CaseObjectAndCaseClassAdt - case class D(s: String) extends CaseObjectAndCaseClassAdt - } - - sealed trait UnionWithNesting - - object UnionWithNesting { - sealed trait Nested extends UnionWithNesting - - object Nested { - case object A extends Nested - case object B extends Nested + ) + + private val optionEncoderSpec = suite("Avro Codec - Encoder Option spec")( + test("Encode Option[Int]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Option[Int]] + val bytes = codec.encode(Some(42)) + assertTrue(bytes.length == 2) + }, + test("Encode List[Option[Int]]") { + val codec = AvroCodec.schemaBasedBinaryCodec[List[Option[Int]]] + val bytes = codec.encode(List(Some(42), Some(53), None, Some(64))) + assertTrue(bytes.length == 10) + }, + test("Encode Chunk[Option[Int]]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[Option[Int]]] + val bytes = codec.encode(Chunk(Some(42), Some(53), None, Some(64))) + assertTrue(bytes.length == 10) } - - @AvroAnnotations.name("MyC") - case object C extends UnionWithNesting - case class D(s: String) extends UnionWithNesting - } - - case class Record(s: String, b: Boolean) - - @AvroAnnotations.name("MyNamedRecord") - case class NamedRecord(s: String, b: Boolean) - - @AvroAnnotations.name("MyNamedFieldRecord") - case class NamedFieldRecord(@AvroAnnotations.name("myNamedField") s: String, b: Boolean) - - @AvroAnnotations.name("NestedRecord") - case class NestedRecord(s: String, nested: NamedRecord) - - @AvroAnnotations.name("Simple") - case class SimpleRecord(s: String) - - object FullAdtTest { - sealed trait TopLevelUnion - - object TopLevelUnion { - case class RecordWithPrimitives( - string: String, - bool: Boolean, - int: Int, - double: Double, - float: Float, - short: Short, - bigInt: BigInt, - bigDecimal: BigDecimal, - unit: Unit, - char: Char, - uuid: UUID - ) extends TopLevelUnion - case class NestedRecord(innerRecord: InnerRecord) extends TopLevelUnion - case class Unions(union: Union) extends TopLevelUnion - case class Enumeration(`enum`: Enum) extends TopLevelUnion - case class Iterables(list: List[String], map: scala.collection.immutable.Map[String, Int]) extends TopLevelUnion - - // TODO: Schema derivation fails for the following case classes - // case class RecordWithTimeRelatedPrimitives(localDateTime: LocalDateTime, localTime: LocalTime, localDate: LocalDate, offsetTime: OffsetTime, offsetDateTime: OffsetDateTime, zonedDateTime: ZonedDateTime, zoneOffset: ZoneOffset, zoneId: ZoneId, instant: Instant) extends TopLevelUnion - // case class IterablesComplex(list: List[InnerRecord], map: Map[InnerRecord, Enum]) extends TopLevelUnion + ) + + private val eitherEncoderSpec = suite("Avro Codec - Encoder Either spec")( + test("Encode Either[Int, String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Either[Int, String]] + val bytes = codec.encode(Right("John")) + assertTrue(bytes.length == 6) + }, + test("Encode Either[Int, String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Either[Int, String]] + val bytes = codec.encode(Left(42)) + assertTrue(bytes.length == 2) + }, + test("Encode Either[List[String], Int]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Either[List[String], Int]] + val bytes = codec.encode(Left(List("John", "Adam", "Daniel"))) + assertTrue(bytes.length == 20) } + ) - case class InnerRecord(s: String, union: Option[scala.util.Either[String, Int]]) - - sealed trait Union - case class NestedUnion(inner: InnerUnion) extends Union - case object OtherCase extends Union + private val tupleEncoderSpec = suite("Avro Codec - Encode Tuples spec")( + test("Encode Tuple2[Int, String]") { + val codec = AvroCodec.schemaBasedBinaryCodec[(Int, String)] + val bytes = codec.encode(Tuple2(42, "Test")) + assertTrue(bytes.length == 6) + } + ) + + private val genericRecordEncoderSpec = suite("Avro Codec - Encode Generic Record")( + test("Encode Record") { + val codec = AvroCodec.schemaBasedBinaryCodec[Record] + val bytes = codec.encode(Record("John", 42)) + assertTrue(bytes.length == 6) + }, + test("Encode High Arity") { + val codec = AvroCodec.schemaBasedBinaryCodec[HighArity] + val bytes = codec.encode(HighArity()) + assertTrue(bytes.length == 24) + } + ) + + private val caseClassEncoderSpec = suite("Avro Codec - Encode Case class")( + test("Encode Case Class 0") { + val codec = AvroCodec.schemaBasedBinaryCodec[Class0] + val bytes = codec.encode(Class0()) + assertTrue(bytes.isEmpty) + }, + test("Encode Case Class 1") { + val codec = AvroCodec.schemaBasedBinaryCodec[Class1] + val bytes = codec.encode(Class1(42)) + assertTrue(bytes.length == 1) + }, + test("Encode Case Class 2") { + val codec = AvroCodec.schemaBasedBinaryCodec[Record] + val bytes = codec.encode(Record("John", 42)) + assertTrue(bytes.length == 6) + }, + test("Encode Case Class 3") { + val ennio = Composer("ennio morricone", "rome", List("legend of 1900", "ecstasy of gold")) + val codec = AvroCodec.schemaBasedBinaryCodec[Composer] + val bytes = codec.encode(ennio) + val expectedResult = (Array[Byte](30, 101, 110, 110, 105, 111, 32, 109, 111, 114, 114, 105, 99, 111, 110, 101, 8, + 114, 111, 109, 101, 4, 28, 108, 101, 103, 101, 110, 100, 32, 111, 102, 32, 49, 57, 48, 48, 30, 101, 99, 115, + 116, 97, 115, 121, 32, 111, 102, 32, 103, 111, 108, 100, 0)) + assertTrue(bytes == Chunk.fromArray(expectedResult)) + } + ) - sealed trait InnerUnion - case class InnerUnionCase1(s: String) extends InnerUnion - case class InnerUnionCase2(i: Int) extends InnerUnion - sealed trait InnerUnionNested extends InnerUnion - case object InnerUnionNestedCase1 extends InnerUnionNested - case object InnerUnionNestedCase2 extends InnerUnionNested + private val enumEncoderSpec = suite("Avro Codec - Encode Enum")( + test("Encode Enum3") { + val codec = AvroCodec.schemaBasedBinaryCodec[OneOf] + val bytes = codec.encode(OneOf.BooleanValue(true)) + assertTrue(bytes.length == 2) + } + ) + + private val primitiveDecoderSpec = suite("Avro Codec - Primitive decoder spec")( + test("Decode Unit") { + val codec = AvroCodec.schemaBasedBinaryCodec[Unit] + val bytes = codec.encode(()) + val result = codec.decode(bytes) + assertTrue(result == Right(())) + }, + test("Decode Boolean") { + val codec = AvroCodec.schemaBasedBinaryCodec[Boolean] + val bytes = codec.encode(true) + val result = codec.decode(bytes) + assertTrue(result == Right(true)) + }, + test("Decode String") { + val codec = AvroCodec.schemaBasedBinaryCodec[String] + val bytes = codec.encode("John") + val result = codec.decode(bytes) + assertTrue(result == Right("John")) + }, + test("Decode Byte") { + val codec = AvroCodec.schemaBasedBinaryCodec[Byte] + val bytes = codec.encode(42.toByte) + val result = codec.decode(bytes) + assertTrue(result == Right(42.toByte)) + }, + test("Decode Short") { + val codec = AvroCodec.schemaBasedBinaryCodec[Short] + val bytes = codec.encode(42.toShort) + val result = codec.decode(bytes) + assertTrue(result == Right(42.toShort)) + }, + test("Decode Integer") { + val codec = AvroCodec.schemaBasedBinaryCodec[Int] + val bytes = codec.encode(42) + val result = codec.decode(bytes) + assertTrue(result == Right(42)) + }, + test("Decode Long") { + val codec = AvroCodec.schemaBasedBinaryCodec[Long] + val bytes = codec.encode(42L) + val result = codec.decode(bytes) + assertTrue(result == Right(42L)) + }, + test("Decode Float") { + val codec = AvroCodec.schemaBasedBinaryCodec[Float] + val bytes = codec.encode(42.0f) + val result = codec.decode(bytes) + assertTrue(result == Right(42.0f)) + }, + test("Decode Double") { + val codec = AvroCodec.schemaBasedBinaryCodec[Double] + val bytes = codec.encode(42.0) + val result = codec.decode(bytes) + assertTrue(result == Right(42.0)) + }, + test("Decode Chunk[Byte]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[Byte]] + val bytes = codec.encode(Chunk.fromArray(Array[Byte](1, 2, 3))) + val result = codec.decode(bytes) + assertTrue(result == Right(Chunk.fromArray(Array[Byte](1, 2, 3)))) + }, + test("Decode Char") { + val codec = AvroCodec.schemaBasedBinaryCodec[Char] + val bytes = codec.encode('a') + val result = codec.decode(bytes) + assertTrue(result == Right('a')) + }, + test("Decode UUID") { + val codec = AvroCodec.schemaBasedBinaryCodec[UUID] + val uuid = UUID.randomUUID() + val bytes = codec.encode(uuid) + val result = codec.decode(bytes) + assertTrue(result == Right(uuid)) + }, + test("Decode BigDecimal") { + val codec = AvroCodec.schemaBasedBinaryCodec[BigDecimal] + val bigDecimal = BigDecimal(42) + val bytes = codec.encode(bigDecimal) + val result = codec.decode(bytes) + assertTrue(result == Right(bigDecimal)) + }, + test("Decode BigInt") { + val codec = AvroCodec.schemaBasedBinaryCodec[BigInt] + val bigInt = BigInt(42) + val bytes = codec.encode(bigInt) + val result = codec.decode(bytes) + assertTrue(result == Right(bigInt)) + }, + test("Decode DayOfWeek") { + val codec = AvroCodec.schemaBasedBinaryCodec[DayOfWeek] + val dayOfWeek = DayOfWeek.FRIDAY + val bytes = codec.encode(dayOfWeek) + val result = codec.decode(bytes) + assertTrue(result == Right(dayOfWeek)) + }, + test("Decode Month") { + val codec = AvroCodec.schemaBasedBinaryCodec[Month] + val month = Month.APRIL + val bytes = codec.encode(month) + val result = codec.decode(bytes) + assertTrue(result == Right(month)) + }, + test("Decode MonthDay") { + val codec = AvroCodec.schemaBasedBinaryCodec[MonthDay] + val monthDay = MonthDay.of(1, 1) + val bytes = codec.encode(monthDay) + val result = codec.decode(bytes) + assertTrue(result == Right(monthDay)) + }, + test("Decode Period") { + val codec = AvroCodec.schemaBasedBinaryCodec[Period] + val period = Period.of(1, 1, 1) + val bytes = codec.encode(period) + val result = codec.decode(bytes) + assertTrue(result == Right(period)) + }, + test("Decode Year") { + val codec = AvroCodec.schemaBasedBinaryCodec[Year] + val year = Year.of(2020) + val bytes = codec.encode(year) + val result = codec.decode(bytes) + assertTrue(result == Right(year)) + }, + test("Decode YearMonth") { + val codec = AvroCodec.schemaBasedBinaryCodec[YearMonth] + val yearMonth = YearMonth.of(2020, 1) + val bytes = codec.encode(yearMonth) + val result = codec.decode(bytes) + assertTrue(result == Right(yearMonth)) + }, + test("Decode ZoneId") { + val codec = AvroCodec.schemaBasedBinaryCodec[ZoneId] + val zoneId = ZoneId.of("UTC") + val bytes = codec.encode(zoneId) + val result = codec.decode(bytes) + assertTrue(result == Right(zoneId)) + }, + test("Decode ZoneOffset") { + val codec = AvroCodec.schemaBasedBinaryCodec[ZoneOffset] + val zoneOffset = ZoneOffset.ofHours(1) + val bytes = codec.encode(zoneOffset) + val result = codec.decode(bytes) + assertTrue(result == Right(zoneOffset)) + }, + test("Decode Duration") { + val codec = AvroCodec.schemaBasedBinaryCodec[Duration] + val duration = Duration.fromMillis(2000) + val bytes = codec.encode(duration) + val result = codec.decode(bytes) + assertTrue(result == Right(duration)) + }, + test("Decode Instant") { + val codec = AvroCodec.schemaBasedBinaryCodec[Instant] + val instant = Instant.now() + val bytes = codec.encode(instant) + val result = codec.decode(bytes) + assertTrue(result == Right(instant)) + }, + test("Decode LocalDate") { + val codec = AvroCodec.schemaBasedBinaryCodec[LocalDate] + val localDate = LocalDate.now() + val bytes = codec.encode(localDate) + val result = codec.decode(bytes) + assertTrue(result == Right(localDate)) + }, + test("Decode LocalDateTime") { + val codec = AvroCodec.schemaBasedBinaryCodec[LocalDateTime] + val localDateTime = LocalDateTime.now() + val bytes = codec.encode(localDateTime) + val result = codec.decode(bytes) + assertTrue(result == Right(localDateTime)) + }, + test("Decode LocalTime") { + val codec = AvroCodec.schemaBasedBinaryCodec[LocalTime] + val localTime = LocalTime.now() + val bytes = codec.encode(localTime) + val result = codec.decode(bytes) + assertTrue(result == Right(localTime)) + }, + test("Decode OffsetDateTime") { + val codec = AvroCodec.schemaBasedBinaryCodec[OffsetDateTime] + val offsetDateTime = OffsetDateTime.now() + val bytes = codec.encode(offsetDateTime) + val result = codec.decode(bytes) + assertTrue(result == Right(offsetDateTime)) + }, + test("Decode OffsetTime") { + val codec = AvroCodec.schemaBasedBinaryCodec[OffsetTime] + val offsetTime = OffsetTime.now() + val bytes = codec.encode(offsetTime) + val result = codec.decode(bytes) + assertTrue(result == Right(offsetTime)) + }, + test("Decode ZonedDateTime") { + val codec = AvroCodec.schemaBasedBinaryCodec[ZonedDateTime] + val zonedDateTime = ZonedDateTime.now() + val bytes = codec.encode(zonedDateTime) + val result = codec.decode(bytes) + assertTrue(result == Right(zonedDateTime)) + } + ) + + private val optionDecoderSpec = suite("Avro Codec - Option Decoder spec")( + test("Decode Option") { + val codec = AvroCodec.schemaBasedBinaryCodec[Option[Int]] + val bytes = codec.encode(Some(42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Some(42))) + } + ) + + private val eitherDecoderSpec = suite("Avro Codec - Either Decoder spec")( + test("Decode Either") { + val codec = AvroCodec.schemaBasedBinaryCodec[Either[String, Int]] + val bytes = codec.encode(Right(42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Right(42))) + }, + test("Decode Either[List[String], Int]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Either[List[String], Int]] + val bytes = codec.encode(Left(List("John", "Adam", "Daniel"))) + val result = codec.decode(bytes) + assertTrue(result == Right(Left(List("John", "Adam", "Daniel")))) + } + ) + + private val tupleDecoderSpec = suite("Avro Codec - Tuple Decoder Spec")( + test("Decode Tuple2") { + val codec = AvroCodec.schemaBasedBinaryCodec[(Int, String)] + val bytes = codec.encode((42, "42")) + val result = codec.decode(bytes) + assertTrue(result == Right((42, "42"))) + } + ) + + private val sequenceDecoderSpec = suite("Avro Codec - Sequence Decoder spec")( + test("Decode List") { + val codec = AvroCodec.schemaBasedBinaryCodec[List[Int]] + val bytes = codec.encode(List(42)) + val result = codec.decode(bytes) + assertTrue(result == Right(List(42))) + }, + test("Decode Set") { + val codec = AvroCodec.schemaBasedBinaryCodec[Set[Int]] + val bytes = codec.encode(Set(42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Set(42))) + }, + test("Decode Map") { + val codec = AvroCodec.schemaBasedBinaryCodec[Map[String, Int]] + val bytes = codec.encode(Map("42" -> 42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Map("42" -> 42))) + }, + test("Decode Chunk") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[String]] + val bytes = codec.encode(Chunk("42", "John", "Adam")) + val result = codec.decode(bytes) + assertTrue(result == Right(Chunk("42", "John", "Adam"))) + }, + test("Decode Chunk[Option[Int]]") { + val codec = AvroCodec.schemaBasedBinaryCodec[Chunk[Option[Int]]] + val bytes = codec.encode(Chunk(Some(42), Some(53), None, Some(64))) + val result = codec.decode(bytes) + assertTrue(result == Right(Chunk(Some(42), Some(53), None, Some(64)))) + } + ) + + private val genericRecordDecoderSpec = suite("Avro Codec - Decode Generic Record")( + test("Decode Record") { + val codec = AvroCodec.schemaBasedBinaryCodec[Record] + val bytes = codec.encode(Record("John", 42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Record("John", 42))) + }, + test("Decode High Arity") { + val codec = AvroCodec.schemaBasedBinaryCodec[HighArity] + val bytes = codec.encode(HighArity()) + val result = codec.decode(bytes) + assertTrue(result == Right(HighArity())) + } + ) + + private val enumDecoderSpec = suite("Avro Codec - Decode enum")( + test("Decode Enum3") { + val codec = AvroCodec.schemaBasedBinaryCodec[OneOf] + val bytes = codec.encode(OneOf.BooleanValue(true)) + val result = codec.decode(bytes) + assertTrue(result == Right(OneOf.BooleanValue(true))) + }, + test("Decode Enum3 - case object") { + val codec = AvroCodec.schemaBasedBinaryCodec[OneOf] + val bytes = codec.encode(OneOf.NullValue) + val result = codec.decode(bytes) + assertTrue(result == Right(OneOf.NullValue)) + }, + test("Decode Enum5") { + val codec = AvroCodec.schemaBasedBinaryCodec[Enums] + val bytes = codec.encode(Enums.A) + val result = codec.decode(bytes) + assertTrue(result == Right(Enums.A)) + }, + test("Decode Person") { + val codec = AvroCodec.schemaBasedBinaryCodec[Person] + val bytes = codec.encode(Person("John", 42)) + val result = codec.decode(bytes) + assertTrue(result == Right(Person("John", 42))) + }, + test("Decode CaseClass3") { + val ennio = Composer("ennio morricone", "rome", List("legend of 1900", "ecstasy of gold")) + val codec = AvroCodec.schemaBasedBinaryCodec[Composer] + val bytes = codec.encode(ennio) + val result = codec.decode(bytes) + assertTrue(result == Right(ennio)) + } + ) + + private val streamEncodingDecodingSpec = + suite("AvroCodec - Stream encode/decode")(test("Encoding/Decoding using streams") { + + val pepperoni = + Pizza("pepperoni", List(Ingredient("pepperoni", 12, 4.4), Ingredient("onions", 1, 0.4)), false, false, 98) + val codec = AvroCodec.schemaBasedBinaryCodec[Pizza] + val resultZIO = ZStream + .fromIterable(List(pepperoni)) + .via(codec.streamEncoder) + .via(codec.streamDecoder) + .runCollect + for { + result <- resultZIO + } yield assertTrue(result == Chunk(pepperoni)) + + }) + + private val genericRecordEncodeDecodeSpec = suite("AvroCodec - encode/decode Generic Record")( + test("Encode/Decode") { + val codec = AvroCodec.schemaBasedBinaryCodec[Record] + val generic: GenericData.Record = codec.encodeGenericRecord(Record("John", 42)) + val result = codec.decodeGenericRecord(generic) + assertTrue(result == Right(Record("John", 42))) + } + ) - sealed trait Enum - case object EnumCase1 extends Enum - case object EnumCase2 extends Enum - } } diff --git a/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroSchemaCodecSpec.scala b/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroSchemaCodecSpec.scala new file mode 100644 index 000000000..25895f44d --- /dev/null +++ b/zio-schema-avro/shared/src/test/scala-2/zio/schema/codec/AvroSchemaCodecSpec.scala @@ -0,0 +1,2051 @@ +package zio.schema.codec + +import java.util.UUID + +import scala.collection.immutable.ListMap +import scala.util.Try + +import zio.schema.Schema._ +import zio.schema._ +import zio.schema.codec.AvroAnnotations.{ BytesType, DecimalType, FieldOrderType } +import zio.test.Assertion._ +import zio.test._ +import zio.{ Chunk, Scope } + +object AvroSchemaCodecSpec extends ZIOSpecDefault { + import AssertionHelper._ + import SpecTestData._ + + override def spec: Spec[Environment with TestEnvironment with Scope, Any] = + suite("AvroSchemaCodecSpec")( + suite("encode")( + suite("enum")( + test("encodes string only enum as avro enum") { + val caseA = Schema.Case[String, String]( + "A", + Schema.primitive(StandardType.StringType), + identity, + identity, + _.isInstanceOf[String] + ) + val caseB = Schema.Case[String, String]( + "B", + Schema.primitive(StandardType.StringType), + identity, + identity, + _.isInstanceOf[String] + ) + val caseC = Schema.Case[String, String]( + "C", + Schema.primitive(StandardType.StringType), + identity, + identity, + _.isInstanceOf[String] + ) + val schema = Schema.Enum3(TypeId.Structural, caseA, caseB, caseC, Chunk(AvroAnnotations.name("MyEnum"))) + + val result = AvroSchemaCodec.encode(schema) + + val expected = """{"type":"enum","name":"MyEnum","symbols":["A","B","C"]}""" + assert(result)(isRight(equalTo(expected))) + }, + test("encodes sealed trait objects only as union of records when no avroEnum annotation is present") { + + val schema = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt] + val result = AvroSchemaCodec.encode(schema) + + val expected = + """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" + assert(result)(isRight(equalTo(expected))) + }, + test("encodes sealed trait objects only as enum when avroEnum annotation is present") { + + val schema = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt].annotate(AvroAnnotations.avroEnum()) + val result = AvroSchemaCodec.encode(schema) + + val expected = """{"type":"enum","name":"MyEnum","symbols":["A","B","MyC"]}""" + assert(result)(isRight(equalTo(expected))) + }, + test("ignores avroEnum annotation if ADT cannot be reduced to String symbols") { + val schema = DeriveSchema.gen[SpecTestData.CaseObjectAndCaseClassAdt] + val result = AvroSchemaCodec.encode(schema) + + val expected = + """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" + assert(result)(isRight(equalTo(expected))) + }, + test("flatten nested unions with initialSchemaDerived derivation") { + val schema = DeriveSchema.gen[SpecTestData.UnionWithNesting] + val result = AvroSchemaCodec.encode(schema) + + val expected = + """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" + assert(result)(isRight(equalTo(expected))) + }, + test("wraps nested unions") { + val schemaA = DeriveSchema.gen[UnionWithNesting.Nested.A.type] + val schemaB = DeriveSchema.gen[UnionWithNesting.Nested.B.type] + val schemaC = DeriveSchema.gen[UnionWithNesting.C.type] + val schemaD = DeriveSchema.gen[UnionWithNesting.D] + + val nested: Enum2[UnionWithNesting.Nested.A.type, UnionWithNesting.Nested.B.type, UnionWithNesting.Nested] = + Schema.Enum2( + TypeId.Structural, + Schema.Case[UnionWithNesting.Nested, UnionWithNesting.Nested.A.type]( + "A", + schemaA, + _.asInstanceOf[UnionWithNesting.Nested.A.type], + _.asInstanceOf[UnionWithNesting.Nested], + _.isInstanceOf[UnionWithNesting.Nested.A.type] + ), + Schema.Case[UnionWithNesting.Nested, UnionWithNesting.Nested.B.type]( + "B", + schemaB, + _.asInstanceOf[UnionWithNesting.Nested.B.type], + _.asInstanceOf[UnionWithNesting.Nested], + _.isInstanceOf[UnionWithNesting.Nested.B.type] + ) + ) + val unionWithNesting = Schema.Enum3( + TypeId.Structural, + Schema.Case[UnionWithNesting, UnionWithNesting.Nested]( + "Nested", + nested, + _.asInstanceOf[UnionWithNesting.Nested], + _.asInstanceOf[UnionWithNesting], + _.isInstanceOf[UnionWithNesting.Nested] + ), + Schema + .Case[UnionWithNesting, UnionWithNesting.C.type]( + "C", + schemaC, + _.asInstanceOf[UnionWithNesting.C.type], + _.asInstanceOf[UnionWithNesting], + _.isInstanceOf[UnionWithNesting.C.type] + ), + Schema.Case[UnionWithNesting, UnionWithNesting.D]( + "D", + schemaD, + _.asInstanceOf[UnionWithNesting.D], + _.asInstanceOf[UnionWithNesting], + _.isInstanceOf[UnionWithNesting.D] + ) + ) + + val schema = unionWithNesting + val result = AvroSchemaCodec.encode(schema) + + val wrappedString = + """[{"type":"record","name":"wrapper_Nested","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true},{"type":"record","name":"C","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" + assert(result)(isRight(equalTo(wrappedString))) + } + ), + suite("record")( + test("generate a static name if not specified via annotation") { + val schema1 = DeriveSchema.gen[SpecTestData.Record] + val schema2 = DeriveSchema.gen[SpecTestData.Record] + val result1 = AvroSchemaCodec.encode(schema1) + val result2 = AvroSchemaCodec.encode(schema2) + + val expected = + """{"type":"record","name":"hashed_1642816955","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + assert(result1)(isRight(equalTo(expected))) && assert(result2)(isRight(equalTo(expected))) + } @@ TestAspect.ignore, // TODO: FIX + test("fail with left on invalid name") { + val schema = DeriveSchema.gen[SpecTestData.Record].annotate(AvroAnnotations.name("0invalid")) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isLeft(containsString("""0invalid"""))) + }, + test("pick up name from annotation") { + val schema = DeriveSchema.gen[SpecTestData.NamedRecord] + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + ) + ) + ) + }, + test("pick up name from annotation for fields") { + val schema = DeriveSchema.gen[SpecTestData.NamedFieldRecord] + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"MyNamedFieldRecord","fields":[{"name":"myNamedField","type":"string"},{"name":"b","type":"boolean"}]}""" + ) + ) + ) + }, + test("pick up doc from annotation") { + val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.doc("My doc")) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"MyNamedRecord","doc":"My doc","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + ) + ) + ) + }, + test("pick up namespace from annotation") { + val schema = + DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.namespace("test.namespace")) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"MyNamedRecord","namespace":"test.namespace","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + ) + ) + ) + }, + test("fail with left on invalid namespace") { + val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.namespace("0@-.invalid")) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isLeft(containsString("""0@-.invalid"""))) + }, + test("pick up error annotation") { + val schema = DeriveSchema.gen[SpecTestData.NamedRecord].annotate(AvroAnnotations.error) + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"error","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + assert(result)(isRight(equalTo(expected))) + }, + test("includes all fields") { + val schema = DeriveSchema.gen[SpecTestData.NamedRecord] + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + assert(result)(isRight(equalTo(expected))) + }, + test("includes nested record fields") { + val schema = DeriveSchema.gen[SpecTestData.NestedRecord] + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"NestedRecord","fields":[{"name":"s","type":"string"},{"name":"nested","type":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" + assert(result)(isRight(equalTo(expected))) + } + ), + suite("map")( + test("string keys and string values") { + val keySchema = Schema.primitive(StandardType.StringType) + val valueSchema = Schema.primitive(StandardType.StringType) + val schema = Schema.Map(keySchema, valueSchema) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"map","values":"string"}"""))) + }, + test("string keys and complex values") { + val keySchema = Schema.primitive(StandardType.StringType) + val valueSchema = DeriveSchema.gen[SpecTestData.SimpleRecord] + val schema = Schema.Map(keySchema, valueSchema) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"map","values":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" + ) + ) + ) + }, + test("complex keys and string values") { + val keySchema = DeriveSchema.gen[SpecTestData.SimpleRecord] + val valueSchema = Schema.primitive(StandardType.StringType) + val schema = Schema.Map(keySchema, valueSchema) + val result = AvroSchemaCodec.encode(schema) + + val isArray = startsWithString("""{"type":"array"""") + val tupleItems = containsString(""""items":{"type":"record","name":"Tuple","namespace":"scala"""") + val hasTupleField_1 = containsString( + """{"name":"_1","type":{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]}}""" + ) + val hasTupleField_2 = containsString("""{"name":"_2","type":"string"}""") + + assert(result)(isRight(isArray && tupleItems && hasTupleField_1 && hasTupleField_2)) + }, + test("complex keys and complex values") { + val keySchema = DeriveSchema.gen[SpecTestData.SimpleRecord] + val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] + val schema = Schema.Map(keySchema, valueSchema) + val result = AvroSchemaCodec.encode(schema) + + val isArray = startsWithString("""{"type":"array"""") + val tupleItems = containsString(""""items":{"type":"record","name":"Tuple","namespace":"scala"""") + val hasTupleField_1 = containsString( + """{"name":"_1","type":{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]}}""" + ) + val hasTupleField_2 = containsString( + """{"name":"_2","type":{"type":"record","name":"MyNamedRecord","namespace":"","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" + ) + + assert(result)(isRight(isArray && tupleItems && hasTupleField_1 && hasTupleField_2)) + } + ), + suite("seq")( + test("is mapped to an avro array") { + val schema = Schema.Sequence[Chunk[String], String, String]( + Schema.primitive(StandardType.StringType), + identity, + identity, + Chunk.empty, + "Seq" + ) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"array","items":"string"}"""))) + }, + test("encodes complex types") { + val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] + val schema = Schema + .Sequence[Chunk[NamedRecord], NamedRecord, String](valueSchema, identity, identity, Chunk.empty, "Seq") + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"array","items":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" + ) + ) + ) + } + ), + suite("set")( + test("is mapped to an avro array") { + val schema = Schema.Sequence[Chunk[String], String, String]( + Schema.primitive(StandardType.StringType), + identity, + identity, + Chunk.empty, + "Seq" + ) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"array","items":"string"}"""))) + }, + test("encodes complex types") { + val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] + val schema = Schema + .Sequence[Chunk[NamedRecord], NamedRecord, String](valueSchema, identity, identity, Chunk.empty, "Seq") + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"array","items":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" + ) + ) + ) + } + ), + suite("optional")( + test("creates a union with case NULL") { + val schema = Schema.Optional(Schema.primitive(StandardType.StringType)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""["null","string"]"""))) + }, + test("encodes complex types") { + val valueSchema = DeriveSchema.gen[SpecTestData.NamedRecord] + val schema = Schema.Optional(valueSchema) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """["null",{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}]""" + ) + ) + ) + }, + test("wraps optional of unit to prevent duplicate null in union") { + val schema = Schema.Optional(Schema.primitive(StandardType.UnitType)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """["null",{"type":"record","name":"wrapper_hashed_3594628","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":"null"}],"zio.schema.codec.avro.wrapper":true}]""" + ) + ) + ) + }, + test("encodes nested optionals") { + val nested = Schema.Optional(Schema.primitive(StandardType.StringType)) + val schema = Schema.Optional(nested) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """["null",{"type":"record","name":"wrapper_hashed_n813828848","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true}]""" + ) + ) + ) + }, + test("encodes optionals of union") { + val union = DeriveSchema.gen[SpecTestData.CaseObjectsOnlyAdt] + val schema = Schema.Optional(union) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """["null",{"type":"record","name":"wrapper_MyEnum","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]},{"type":"record","name":"MyC","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true}]""" + ) + ) + ) + }, + test("encodes optionals of either") { + val either = + Schema.Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) + val schema = Schema.Optional(either) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """["null",{"type":"record","name":"wrapper_hashed_n630422444","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}]""" + ) + ) + ) + } + ), + suite("either")( + test("create an union") { + val schema = + Schema.Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"wrapper_hashed_n630422444","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}""" + assert(result)(isRight(equalTo(expected))) + }, + test("create a named union") { + val schema = Schema + .Either(Schema.primitive(StandardType.StringType), Schema.primitive(StandardType.IntType)) + .annotate(AvroAnnotations.name("MyEither")) + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"wrapper_MyEither","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":["string","int"]}],"zio.schema.codec.avro.either":true}""" + assert(result)(isRight(equalTo(expected))) + }, + test("encodes complex types") { + val left = DeriveSchema.gen[SpecTestData.SimpleRecord] + val right = Schema.primitive(StandardType.StringType) + val schema = Schema.Either(left, right) + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"wrapper_hashed_754352222","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"Simple","namespace":"","fields":[{"name":"s","type":"string"}]},"string"]}],"zio.schema.codec.avro.either":true}""" + assert(result)(isRight(equalTo(expected))) + }, + test("fails with duplicate names") { + val left = DeriveSchema.gen[SpecTestData.SimpleRecord] + val right = DeriveSchema.gen[SpecTestData.SimpleRecord] + val schema = Schema.Either(left, right) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isLeft(equalTo("""Left and right schemas of either must have different fullnames: Simple""")) + ) + }, + test("encodes either containing optional") { + val left = Schema.Optional(Schema.primitive(StandardType.StringType)) + val right = Schema.primitive(StandardType.StringType) + val schema = Schema.Either(left, right) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"wrapper_hashed_n465006219","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true}""" + ) + ) + ) + }, + test("encodes nested either") { + val left = Schema.Optional(Schema.primitive(StandardType.StringType)) + val right = Schema.primitive(StandardType.StringType) + val nested = Schema.Either(left, right) + val schema = Schema.Either(nested, right) + val result = AvroSchemaCodec.encode(schema) + + val expected = + """{"type":"record","name":"wrapper_hashed_2071802344","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n465006219","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true},"string"]}],"zio.schema.codec.avro.either":true}""" + assert(result)(isRight(equalTo(expected))) + } + ), + suite("tuple")( + test("creates a record type and applies the name") { + val left = Schema.primitive(StandardType.StringType) + val right = Schema.primitive(StandardType.StringType) + val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"record","name":"MyTuple","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"string"}],"zio.schema.codec.recordType":"tuple"}""" + ) + ) + ) + }, + test("encodes complex types") { + val left = DeriveSchema.gen[SpecTestData.SimpleRecord] + val right = DeriveSchema.gen[SpecTestData.NamedRecord] + val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) + val result = AvroSchemaCodec.encode(schema) + + val field_1 = + """{"name":"_1","type":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" + val field_2 = + """{"name":"_2","type":{"type":"record","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}""" + assert(result)(isRight(containsString(field_1) && containsString(field_2))) + }, + test("encodes duplicate complex types by reference") { + val left = DeriveSchema.gen[SpecTestData.SimpleRecord] + val right = DeriveSchema.gen[SpecTestData.SimpleRecord] + val schema = Schema.Tuple2(left, right).annotate(AvroAnnotations.name("MyTuple")) + val result = AvroSchemaCodec.encode(schema) + + val field_1 = + """{"name":"_1","type":{"type":"record","name":"Simple","fields":[{"name":"s","type":"string"}]}}""" + val field_2 = """{"name":"_2","type":"Simple"}""" + assert(result)(isRight(containsString(field_1) && containsString(field_2))) + } + ), + suite("primitives")( + test("encodes UnitType") { + val schema = Schema.primitive(StandardType.UnitType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"null\""))) + }, + test("encodes StringType") { + val schema = Schema.primitive(StandardType.StringType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"string\""))) + }, + test("encodes BooleanType") { + val schema = Schema.primitive(StandardType.BoolType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"boolean\""))) + }, + test("encodes ShortType") { + val schema = Schema.primitive(StandardType.ShortType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"short"}"""))) + }, + test("encodes IntType") { + val schema = Schema.primitive(StandardType.IntType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"int\""))) + }, + test("encodes LongType") { + val schema = Schema.primitive(StandardType.LongType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"long\""))) + }, + test("encodes FloatType") { + val schema = Schema.primitive(StandardType.FloatType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"float\""))) + }, + test("encodes DoubleType") { + val schema = Schema.primitive(StandardType.DoubleType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"double\""))) + }, + test("encodes BinaryType as bytes") { + val schema = Schema.primitive(StandardType.BinaryType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"bytes\""))) + }, + test("encodes BinaryType as fixed") { + val size = 12 + val schema = + Schema + .primitive(StandardType.BinaryType) + .annotate(AvroAnnotations.bytes(BytesType.Fixed(size, "MyFixed"))) + val result = AvroSchemaCodec.encode(schema) + + val expected = """{"type":"fixed","name":"MyFixed","doc":"","size":12}""" + assert(result)(isRight(equalTo(expected))) + }, + test("encodes CharType") { + val schema = Schema.primitive(StandardType.CharType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"char"}"""))) + }, + test("encodes UUIDType") { + val schema = Schema.primitive(StandardType.UUIDType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"string","logicalType":"uuid"}"""))) + }, + test("encodes BigDecimalType as Bytes") { + val schema = + Schema.primitive(StandardType.BigDecimalType).annotate(AvroAnnotations.decimal(DecimalType.Bytes)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":48,"scale":24}"""))) + }, + test("encodes BigDecimalType as Bytes with scala and precision") { + val schema = Schema + .primitive(StandardType.BigDecimalType) + .annotate(AvroAnnotations.decimal(DecimalType.Bytes)) + .annotate(AvroAnnotations.scale(10)) + .annotate(AvroAnnotations.precision(20)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":20,"scale":10}"""))) + }, + test("encodes BigDecimalType as Fixed") { + val schema = + Schema.primitive(StandardType.BigDecimalType).annotate(AvroAnnotations.decimal(DecimalType.Fixed(21))) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"fixed","name":"Decimal_48_24","size":21,"logicalType":"decimal","precision":48,"scale":24}""" + ) + ) + ) + }, + test("encodes BigDecimalType as Fixed with scala and precision") { + val schema = Schema + .primitive(StandardType.BigDecimalType) + .annotate(AvroAnnotations.decimal(DecimalType.Fixed(9))) + .annotate(AvroAnnotations.scale(10)) + .annotate(AvroAnnotations.precision(20)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"fixed","name":"Decimal_20_10","size":9,"logicalType":"decimal","precision":20,"scale":10}""" + ) + ) + ) + }, + test("encodes BigIntegerType as Bytes") { + val schema = + Schema.primitive(StandardType.BigIntegerType).annotate(AvroAnnotations.decimal(DecimalType.Bytes)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":24,"scale":24}"""))) + }, + test("encodes BigIntegerType as Bytes with scala and precision") { + val schema = Schema + .primitive(StandardType.BigIntegerType) + .annotate(AvroAnnotations.decimal(DecimalType.Bytes)) + .annotate(AvroAnnotations.scale(10)) + .annotate(AvroAnnotations.precision(20)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"bytes","logicalType":"decimal","precision":10,"scale":10}"""))) + }, + test("encodes BigIntegerType as Fixed") { + val schema = + Schema.primitive(StandardType.BigIntegerType).annotate(AvroAnnotations.decimal(DecimalType.Fixed(11))) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"fixed","name":"Decimal_24_24","size":11,"logicalType":"decimal","precision":24,"scale":24}""" + ) + ) + ) + }, + test("encodes BigIntegerType as Fixed with scala and precision") { + val schema = Schema + .primitive(StandardType.BigIntegerType) + .annotate(AvroAnnotations.decimal(DecimalType.Fixed(5))) + .annotate(AvroAnnotations.scale(10)) + .annotate(AvroAnnotations.precision(20)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":10,"scale":10}""" + ) + ) + ) + }, + test("encodes DayOfWeekType") { + val schema = Schema.primitive(StandardType.DayOfWeekType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"dayOfWeek"}"""))) + }, + test("encodes MonthType") { + val schema = Schema.primitive(StandardType.MonthType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"month"}"""))) + }, + test("encodes YearType") { + val schema = Schema.primitive(StandardType.YearType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"year"}"""))) + }, + test("encodes ZoneIdType") { + val schema = Schema.primitive(StandardType.ZoneIdType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"string","zio.schema.codec.stringType":"zoneId"}"""))) + }, + test("encodes ZoneOffsetType") { + val schema = Schema.primitive(StandardType.ZoneOffsetType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("""{"type":"int","zio.schema.codec.intType":"zoneOffset"}"""))) + }, + //TODO 1 + //test("encodes MonthDayType") { + // val schema = Schema.primitive(StandardType.MonthDayType) + // val result = AvroSchemaCodec.encode(schema) + + // assert(result)( + // isRight( + // equalTo( + // """{"type":"record","name":"MonthDay","namespace":"zio.schema.codec.avro","fields":[{"name":"month","type":"int"},{"name":"day","type":"int"}],"zio.schema.codec.recordType":"monthDay"}""" + // ) + // ) + // ) + //}, + //TODO 2 + //test("encodes PeriodType") { + // val schema = Schema.primitive(StandardType.PeriodType) + // val result = AvroSchemaCodec.encode(schema) + // + // assert(result)( + // isRight( + // equalTo( + // """{"type":"record","name":"Period","namespace":"zio.schema.codec.avro","fields":[{"name":"years","type":"int"},{"name":"months","type":"int"},{"name":"days","type":"int"}],"zio.schema.codec.recordType":"period"}""" + // ) + // ) + // ) + //}, + //TODO 3 + //test("encodes YearMonthType") { + // val schema = Schema.primitive(StandardType.YearMonthType) + // val result = AvroSchemaCodec.encode(schema) + // + // assert(result)( + // isRight( + // equalTo( + // """{"type":"record","name":"YearMonth","namespace":"zio.schema.codec.avro","fields":[{"name":"year","type":"int"},{"name":"month","type":"int"}],"zio.schema.codec.recordType":"yearMonth"}""" + // ) + // ) + // ) + //}, + //TODO 4 + //test("encodes Duration") { + // val schema = Schema.primitive(StandardType.DurationType) // .duration(ChronoUnit.DAYS)) + // val result = AvroSchemaCodec.encode(schema) + // + // assert(result)( + // isRight( + // equalTo( + // """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration","zio.schema.codec.avro.durationChronoUnit":"DAYS"}""" + // ) + // ) + // ) + //}, + test("encodes InstantType as string") { + val schema = Schema.primitive(StandardType.InstantType).annotate(AvroAnnotations.formatToString) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"instant"}""" + ) + ) + ) + }, + test("encodes LocalDateType as string") { + val schema = + Schema.primitive(StandardType.LocalDateType).annotate(AvroAnnotations.formatToString) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"localDate"}""" + ) + ) + ) + }, + test("encodes LocalTimeType as string") { + val schema = + Schema.primitive(StandardType.LocalTimeType).annotate(AvroAnnotations.formatToString) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"localTime"}""" + ) + ) + ) + }, + test("encodes LocalDateTimeType as string") { + val schema = + Schema.primitive(StandardType.LocalDateTimeType).annotate(AvroAnnotations.formatToString) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"localDateTime"}""" + ) + ) + ) + }, + test("encodes OffsetTimeType") { + val schema = Schema.primitive(StandardType.OffsetTimeType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"offsetTime"}""" + ) + ) + ) + }, + test("encodes OffsetDateTimeType") { + val schema = Schema.primitive(StandardType.OffsetDateTimeType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"offsetDateTime"}""" + ) + ) + ) + }, + test("encodes ZonedDateTimeType") { + val schema = Schema.primitive(StandardType.ZonedDateTimeType) + val result = AvroSchemaCodec.encode(schema) + + assert(result)( + isRight( + equalTo( + """{"type":"string","zio.schema.codec.stringType":"zoneDateTime"}""" + ) + ) + ) + } + ), + test("fail should fail the encode") { + val schema = Schema.fail("I'm failing") + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isLeft(equalTo("""I'm failing"""))) + }, + test("lazy is handled properly") { + val schema = Schema.Lazy(() => Schema.primitive(StandardType.StringType)) + val result = AvroSchemaCodec.encode(schema) + + assert(result)(isRight(equalTo("\"string\""))) + } + ), + /** + * Test Decoder + */ + suite("decode")( + suite("record")( + test("decode a simple record") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema.map(_.ast))( + isRight( + equalTo( + Schema + .record( + TypeId.fromTypeName("TestRecord"), + Schema.Field( + "s", + Schema.primitive(StandardType.StringType), + get0 = (p: ListMap[String, _]) => p("s").asInstanceOf[String], + set0 = (p: ListMap[String, _], v: String) => p.updated("s", v) + ), + Schema.Field( + "b", + Schema.primitive(StandardType.BoolType), + get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], + set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) + ) + ) + .ast + ) + ) + ) + }, + test("decode a nested record") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"nested","type":{"type":"record","name":"Inner","fields":[{"name":"innerS","type":"string"}]}},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + val expectedSchema = Schema.record( + TypeId.fromTypeName("TestRecord"), + Schema.Field( + "nested", + Schema.record( + TypeId.fromTypeName("Inner"), + Schema.Field( + "innerS", + Schema.primitive(StandardType.StringType), + get0 = (p: ListMap[String, _]) => p("innerS").asInstanceOf[String], + set0 = (p: ListMap[String, _], v: String) => p.updated("innerS", v) + ) + ), + get0 = (p: ListMap[String, _]) => p("nested").asInstanceOf[ListMap[String, _]], + set0 = (p: ListMap[String, _], v: ListMap[String, _]) => p.updated("nested", v) + ), + Schema.Field( + "b", + Schema.primitive(StandardType.BoolType), + get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], + set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) + ) + ) + + assert(schema.map(_.ast))(isRight(equalTo(expectedSchema.ast))) + }, + test("unwrap a wrapped initialSchemaDerived") { + val s = + """{"type":"record","zio.schema.codec.avro.wrapper":true,"name":"wrapper_xyz","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema.map(_.ast))( + isRight( + equalTo( + Schema + .record( + TypeId.fromTypeName("TestRecord"), + Schema.Field( + "s", + Schema.primitive(StandardType.StringType), + get0 = (p: ListMap[String, _]) => p("s").asInstanceOf[String], + set0 = (p: ListMap[String, _], v: String) => p.updated("s", v) + ), + Schema.Field( + "b", + Schema.primitive(StandardType.BoolType), + get0 = (p: ListMap[String, _]) => p("b").asInstanceOf[Boolean], + set0 = (p: ListMap[String, _], v: Boolean) => p.updated("b", v) + ) + ) + .ast + ) + ) + ) + }, + test("period record") { + val s = + """{"type":"record","name":"Period","namespace":"zio.schema.codec.avro","fields":[{"name":"years","type":"int"},{"name":"months","type":"int"},{"name":"days","type":"int"}],"zio.schema.codec.recordType":"period"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.PeriodType))) + }, + test("yearMonth record") { + val s = + """{"type":"record","name":"YearMonth","namespace":"zio.schema.codec.avro","fields":[{"name":"year","type":"int"},{"name":"month","type":"int"}],"zio.schema.codec.recordType":"yearMonth"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.YearMonthType))) + }, + test("tuple record successful") { + val s = + """{"type":"record","name":"Tuple","namespace":"zio.schema.codec.avro","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"int"}],"zio.schema.codec.recordType":"tuple"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isTuple(isStandardType(StandardType.StringType), isStandardType(StandardType.IntType))) + ) + }, + test("tuple record failing") { + val s = + """{"type":"record","name":"Tuple","namespace":"zio.schema.codec.avro","fields":[{"name":"_1","type":"string"},{"name":"_2","type":"int"},{"name":"_3","type":"int"}],"zio.schema.codec.recordType":"tuple"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isLeft) + }, + test("monthDay record") { + val s = + """{"type":"record","name":"MonthDay","namespace":"zio.schema.codec.avro","fields":[{"name":"month","type":"int"},{"name":"day","type":"int"}],"zio.schema.codec.recordType":"monthDay"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.MonthDayType))) + }, + test("duration record without chrono unit annotation") { + val s = + """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.DurationType))) //(ChronoUnit.MILLIS)))) + }, + test("duration record chrono unit annotation") { + val s = + """{"type":"record","name":"Duration","namespace":"zio.schema.codec.avro","fields":[{"name":"seconds","type":"long"},{"name":"nanos","type":"int"}],"zio.schema.codec.recordType":"duration","zio.schema.codec.avro.durationChronoUnit":"DAYS"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.DurationType))) //(ChronoUnit.DAYS)))) + }, + test("assign the name annotation") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasNameAnnotation(equalTo("TestRecord"))))) + }, + test("assign the namespace annotation") { + val s = + """{"type":"record","name":"TestRecord","namespace":"MyTest","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasNamespaceAnnotation(equalTo("MyTest"))))) + }, + test("not assign the namespace annotation if missing") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasNamespaceAnnotation(anything).negate))) + }, + zio.test.test("assign the doc annotation") { + val s = + """{"type":"record","name":"TestRecord","doc":"Very helpful documentation!","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasDocAnnotation(equalTo("Very helpful documentation!"))))) + }, + test("not assign the doc annotation if missing") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasDocAnnotation(anything).negate))) + }, + test("assign the aliases annotation") { + val s = + """{"type":"record","name":"TestRecord","aliases":["wow", "cool"],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isRecord(hasAliasesAnnotation(exists[String](equalTo("wow")) && exists(equalTo("cool"))))) + ) + }, + test("not assign the aliases annotation if missing") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasAliasesAnnotation(anything).negate))) + }, + test("not assign the aliases annotation if empty") { + val s = + """{"type":"record","name":"TestRecord","aliases":[],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasAliasesAnnotation(anything).negate))) + }, + zio.test.test("assign the error annotation") { + val s = + """{"type":"error","name":"MyNamedRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasErrorAnnotation))) + }, + test("not assign the error annotation if not an error") { + val s = + """{"type":"record","name":"TestRecord","aliases":[],"fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isRecord(hasErrorAnnotation.negate))) + } + ), + suite("fields")( + test("decodes primitive fields of record") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField(hasLabel(equalTo("s")) && hasSchema(isStandardType(StandardType.StringType))) + val field2 = hasRecordField(hasLabel(equalTo("b")) && hasSchema(isStandardType(StandardType.BoolType))) + assert(schema)(isRight(isRecord(field1 && field2))) + }, + test("decodes the fields complex initialSchemaDerived") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"complex","type":{"type":"record","name":"Complex","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField(hasLabel(equalTo("s")) && hasSchema(isStandardType(StandardType.StringType))) + val field2 = hasRecordField(hasLabel(equalTo("b")) && hasSchema(isStandardType(StandardType.BoolType))) + val complex = isRecord(field1 && field2) + val field = hasRecordField(hasLabel(equalTo("complex")) && hasSchema(complex)) + assert(schema)(isRight(isRecord(field))) + }, + zio.test.test("assign the field name annotation") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField(hasLabel(equalTo("s")) && hasNameAnnotation(equalTo("s"))) + val field2 = hasRecordField(hasLabel(equalTo("b")) && hasNameAnnotation(equalTo("b"))) + assert(schema)(isRight(isRecord(field1 && field2))) + }, + zio.test.test("assign the field doc annotation iff it exists") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","doc":"Very helpful doc!","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField(hasLabel(equalTo("s")) && hasDocAnnotation(equalTo("Very helpful doc!"))) + val field2 = hasRecordField(hasLabel(equalTo("b")) && hasDocAnnotation(anything).negate) + assert(schema)(isRight(isRecord(field1 && field2))) + }, + test("assign the field default annotation") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","default":"defaultValue","type":"string"},{"name":"complex","default":{"s":"defaultS","b":true},"type":{"type":"record","name":"Complex","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]}},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField(hasLabel(equalTo("s")) && hasFieldDefaultAnnotation(equalTo("defaultValue"))) + val field2 = hasRecordField( + hasLabel(equalTo("complex")) && hasFieldDefaultAnnotation(asString(equalTo("""{s=defaultS, b=true}"""))) + ) + val field3 = hasRecordField(hasLabel(equalTo("b")) && hasFieldDefaultAnnotation(anything).negate) + assert(schema)(isRight(isRecord(field1 && field2 && field3))) + }, + zio.test.test("assign the fieldOrder annotation") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","order":"descending","type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField( + hasLabel(equalTo("s")) && hasFieldOrderAnnotation(equalTo(AvroAnnotations.FieldOrderType.Descending)) + ) + val field2 = hasRecordField( + hasLabel(equalTo("b")) && hasFieldOrderAnnotation(equalTo(AvroAnnotations.FieldOrderType.Ascending)) + ) + assert(schema)(isRight(isRecord(field1 && field2))) + }, + zio.test.test("assign the field aliases annotation") { + val s = + """{"type":"record","name":"TestRecord","fields":[{"name":"s","aliases":["wow", "cool"],"type":"string"},{"name":"b","type":"boolean"}]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val field1 = hasRecordField( + hasLabel(equalTo("s")) && hasAliasesAnnotation(Assertion.hasSameElements(Seq("wow", "cool"))) + ) + val field2 = hasRecordField(hasLabel(equalTo("b")) && hasAliasesAnnotation(anything).negate) + assert(schema)(isRight(isRecord(field1 && field2))) + } + ), + suite("enum")( + test("decodes symbols as union of strings") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val symbolKeysAssetion = Assertion.hasKeys(hasSameElements(Seq("a", "b", "c"))) + val enumStringTypeAssertion: Assertion[ListMap[String, (Schema[_], Chunk[Any])]] = + Assertion.hasValues(forall(tuple2First(isStandardType(StandardType.StringType)))) + assert(schema)(isRight(isEnum(enumStructure(symbolKeysAssetion && enumStringTypeAssertion)))) + }, + test("assign the enum name annotation") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasNameAnnotation(equalTo("TestEnum"))))) + }, + test("assign the enum namespace annotation") { + val s = """{"type":"enum","name":"TestEnum","namespace":"MyTest","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasNamespaceAnnotation(equalTo("MyTest"))))) + }, + test("not assign the enum namespace annotation if empty") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasNamespaceAnnotation(anything).negate))) + }, + test("assign the enum aliases annotation") { + val s = """{"type":"enum","name":"TestEnum","aliases":["MyAlias", "MyAlias2"],"symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasAliasesAnnotation(hasSameElements(Seq("MyAlias", "MyAlias2")))))) + }, + test("not assign the enum aliases annotation if empty") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasAliasesAnnotation(anything).negate))) + }, + test("assign the enum doc annotation") { + val s = + """{"type":"enum","name":"TestEnum","doc":"Some very helpful documentation!","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasDocAnnotation(equalTo("Some very helpful documentation!"))))) + }, + test("not assign the enum doc annotation if empty") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasAliasesAnnotation(anything).negate))) + }, + test("assign the enum default annotation") { + val s = """{"type":"enum","name":"TestEnum","default":"a","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasDefaultAnnotation(equalTo("a"))))) + }, + test("fail if enum default is not a symbol") { + val s = """{"type":"enum","name":"TestEnum","default":"d","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isLeft(equalTo("The Enum Default: d is not in the enum symbol set: [a, b, c]"))) + }, + test("not assign the enum default annotation if empty") { + val s = """{"type":"enum","name":"TestEnum","symbols":["a","b","c"]}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isEnum(hasDefaultAnnotation(anything).negate))) + } + ), + test("decodes primitive array") { + val s = """{"type":"array","items":{"type":"int"}}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isSequence(hasSequenceElementSchema(isStandardType(StandardType.IntType))))) + }, + test("decodes complex array") { + val s = + """{"type":"array","items":{"type":"record","name":"TestRecord","fields":[{"name":"f1","type":"int"},{"name":"f2","type":"string"}]}}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isSequence(hasSequenceElementSchema(isRecord(anything))))) + }, + test("decodes map with string keys") { + val s = """{"type":"map","values":{"type":"int"}}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight( + isMap( + hasMapKeys(isStandardType(StandardType.StringType)) && hasMapValues( + isStandardType(StandardType.IntType) + ) + ) + ) + ) + }, + suite("union")( + test("option union with null on first position") { + val s = """[{"type":"null"}, {"type":"int"}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isOption(hasOptionElementSchema(isStandardType(StandardType.IntType))))) + }, + test("option union with null on second position") { + val s = """[{"type":"int"}, {"type":"null"}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isOption(hasOptionElementSchema(isStandardType(StandardType.IntType))))) + }, + test("not an option union with more than one element type") { + val s = """[{"type":"null"}, {"type":"int"}, {"type":"string"}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isOption(anything).negate)) + }, + test("nested either union") { + val s = + """{"type":"record","name":"wrapper_hashed_2071802344","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n465006219","fields":[{"name":"value","type":[{"type":"record","name":"wrapper_hashed_n813828848","fields":[{"name":"value","type":["null","string"]}],"zio.schema.codec.avro.wrapper":true},"string"]}],"zio.schema.codec.avro.either":true},"string"]}],"zio.schema.codec.avro.either":true}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight( + isEither( + isEither(isOption(anything), isStandardType(StandardType.StringType)), + isStandardType(StandardType.StringType) + ) + ) + ) + }, + test("union as zio initialSchemaDerived enumeration") { + val s = """[{"type":"null"}, {"type":"int"}, {"type":"string"}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val assertion1 = hasKey("null", tuple2First(isStandardType(StandardType.UnitType))) + val sssertion2 = hasKey("int", tuple2First(isStandardType(StandardType.IntType))) + val assertion3 = hasKey("string", tuple2First(isStandardType(StandardType.StringType))) + assert(schema)(isRight(isEnum(enumStructure(assertion1 && sssertion2 && assertion3)))) + }, + test("correct case codec for case object of ADT") { + val s = + """[{"type":"record","name":"A","fields":[]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val assertionA = hasKey("A", tuple2First(isEmptyRecord)) + val assertionB = hasKey("B", tuple2First(isEmptyRecord)) + val assertionMyC = hasKey("MyC", tuple2First(isEmptyRecord)) + assert(schema)(isRight(isEnum(enumStructure(assertionA && assertionB && assertionMyC)))) + }, + test("correct case codec for case class of ADT") { + val s = + """[{"type":"record","name":"A","fields":[{"name":"s","type":"string"},{"name":"b","type":"boolean"}]},{"type":"record","name":"B","fields":[]},{"type":"record","name":"MyC","fields":[]}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val assertionA = hasKey( + "A", + tuple2First(isRecord(hasRecordField(hasLabel(equalTo("s"))) && hasRecordField(hasLabel(equalTo("b"))))) + ) + val assertionB = hasKey("B", tuple2First(isEmptyRecord)) + val assertionMyC = hasKey("MyC", tuple2First(isEmptyRecord)) + assert(schema)(isRight(isEnum(enumStructure(assertionA && assertionB && assertionMyC)))) + }, + test("unwrap nested union") { + val s = + """[{"type":"record","name":"wrapper_hashed_n465006219","namespace":"zio.schema.codec.avro","fields":[{"name":"value","type":[{"type":"record","name":"A","namespace":"","fields":[]},{"type":"record","name":"B","namespace":"","fields":[]}]}],"zio.schema.codec.avro.wrapper":true},{"type":"record","name":"C","fields":[]},{"type":"record","name":"D","fields":[{"name":"s","type":"string"}]}]""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val nestedEnumAssertion = isEnum( + enumStructure( + hasKey("A", tuple2First(isEmptyRecord)) && hasKey( + "B", + tuple2First(isEmptyRecord) + ) + ) + ) + val nestedEnumKey = + hasKey("zio.schema.codec.avro.wrapper_hashed_n465006219", tuple2First(nestedEnumAssertion)) + val cEnumKey = hasKey("C", tuple2First(isEmptyRecord)) + val dEnumKey = hasKey("D", tuple2First(isRecord(hasRecordField(hasLabel(equalTo("s")))))) + assert(schema)(isRight(isEnum(enumStructure(nestedEnumKey && cEnumKey && dEnumKey)))) + } + ), + suite("fixed")( + test("logical type decimal as BigDecimal") { + val s = + """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":11,"scale":10}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val isDecimalAssertion = isStandardType(StandardType.BigDecimalType) + val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = + exists(equalTo(AvroAnnotations.decimal(DecimalType.Fixed(5)))) + val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) + val hasPrecisionAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.precision(11))) + val hasAnnotationsAssertion = + annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && hasPrecisionAnnotation) + assert(schema)(isRight(isDecimalAssertion && hasAnnotationsAssertion)) + }, + test("logical type decimal as BigInteger") { + val s = + """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":10,"scale":10}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val isBigIntegerType = isStandardType(StandardType.BigIntegerType) + val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = + exists(equalTo(AvroAnnotations.decimal(DecimalType.Fixed(5)))) + val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) + val doesNotHavePrecisionAnnotation: Assertion[Iterable[Any]] = + exists(Assertion.isSubtype[AvroAnnotations.precision.type](anything)).negate + val hasAnnotationsAssertion = + annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && doesNotHavePrecisionAnnotation) + assert(schema)(isRight(isBigIntegerType && hasAnnotationsAssertion)) + }, + test("fail on invalid logical type") { + val s = + """{"type":"fixed","name":"Decimal_10_10","size":5,"logicalType":"decimal","precision":9,"scale":10}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isLeft(equalTo("Invalid decimal scale: 10 (greater than precision: 9)"))) + }, + test("decode as binary") { + val s = """{"type":"fixed","name":"Something","size":5}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val hasNameAnnotation = annotations(exists(equalTo(AvroAnnotations.name("Something")))) + assert(schema)(isRight(isStandardType(StandardType.BinaryType) && hasNameAnnotation)) + } + ), + suite("string")( + test("decodes zoneId with formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"zoneId"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.ZoneIdType))) + }, + test("decodes instant with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"ISO_INSTANT"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes instant using default") { + val s = """{"type":"string","zio.schema.codec.stringType":"instant"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes instant with formatter pattern") { + val pattern = "yyyy MM dd" + val s = + s"""{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"$pattern"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decode DateTimeFormatter field fails on invalid formatter") { + val pattern = "this is not a valid formatter pattern" + val s = + s"""{"type":"string","zio.schema.codec.stringType":"instant","zio.schema.codec.avro.dateTimeFormatter":"$pattern"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isLeft(equalTo("Unknown pattern letter: t"))) + }, + test("decodes localDate with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"localDate","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) + }, + test("decodes localDate with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"localDate"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) + }, + test("decodes localTime with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"localTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes localTime with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"localTime"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes localDateTime with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"localDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) + }, + test("decodes localDateTime with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"localDateTime"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isStandardType(StandardType.LocalDateTimeType)) + ) + }, + test("decodes zonedDateTime with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"zoneDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.ZonedDateTimeType))) + }, + test("decodes zonedDateTime with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"zoneDateTime"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isStandardType(StandardType.ZonedDateTimeType)) + ) + }, + test("decodes offsetTime with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"offsetTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.OffsetTimeType))) + }, + test("decodes offsetTime with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"offsetTime"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.OffsetTimeType))) + }, + test("decodes offsetDateTime with formatter") { + val s = + """{"type":"string","zio.schema.codec.stringType":"offsetDateTime","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.OffsetDateTimeType))) + }, + test("decodes offsetDateTime with default formatter") { + val s = """{"type":"string","zio.schema.codec.stringType":"offsetDateTime"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isStandardType(StandardType.OffsetDateTimeType)) + ) + }, + test("decodes logical type uuid") { + val s = """{"type":"string","logicalType":"uuid"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.UUIDType))) + }, + test("decodes primitive type string") { + val s = """{"type":"string"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.StringType))) + } + ), + suite("bytes")( + test("logical type decimal as BigDecimal") { + val s = """{"type":"bytes","logicalType":"decimal","precision":20,"scale":10}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val isDecimalAssertion = isStandardType(StandardType.BigDecimalType) + val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = + exists(equalTo(AvroAnnotations.decimal(DecimalType.Bytes))) + val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(10))) + val hasPrecisionAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.precision(20))) + val hasAnnotationsAssertion = + annotations(hasDecimalTypeAnnotation && hasScalaAnnotation && hasPrecisionAnnotation) + assert(schema)(isRight(isDecimalAssertion && hasAnnotationsAssertion)) + }, + test("logical type decimal as BigInteger") { + val s = """{"type":"bytes","logicalType":"decimal","precision":20,"scale":20}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + val isBigIntegerAssertion = isStandardType(StandardType.BigIntegerType) + val hasDecimalTypeAnnotation: Assertion[Iterable[Any]] = + exists(equalTo(AvroAnnotations.decimal(DecimalType.Bytes))) + val hasScalaAnnotation: Assertion[Iterable[Any]] = exists(equalTo(AvroAnnotations.scale(20))) + val hasAnnotationsAssertion = annotations(hasDecimalTypeAnnotation && hasScalaAnnotation) + assert(schema)(isRight(isBigIntegerAssertion && hasAnnotationsAssertion)) + }, + test("decode as binary") { + val s = """{"type":"bytes"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.BinaryType))) + } + ), + suite("int")( + test("decodes char") { + val s = """{"type":"int","zio.schema.codec.intType":"char"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.CharType))) + }, + test("decodes dayOfWeek") { + val s = """{"type":"int","zio.schema.codec.intType":"dayOfWeek"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.DayOfWeekType))) + }, + test("decodes Year") { + val s = """{"type":"int","zio.schema.codec.intType":"year"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.YearType))) + }, + test("decodes short") { + val s = """{"type":"int","zio.schema.codec.intType":"short"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.ShortType))) + }, + test("decodes month") { + val s = """{"type":"int","zio.schema.codec.intType":"month"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.MonthType))) + }, + test("decodes zoneOffset") { + val s = """{"type":"int","zio.schema.codec.intType":"zoneOffset"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.ZoneOffsetType))) + }, + test("decodes int") { + val s = """{"type":"int"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.IntType))) + }, + test("decodes logical type timemillis") { + val s = + """{"type":"int","logicalType":"time-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes logical type timemillis with default formatter") { + val s = """{"type":"int","logicalType":"time-millis"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes logical type date") { + val s = + """{"type":"int","logicalType":"date"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) + }, + test("decodes logical type date with default formatter") { + val s = """{"type":"int","logicalType":"date"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateType))) + } + ), + suite("long")( + test("decodes long") { + val s = """{"type":"long"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LongType))) + }, + test("decodes logical type timeMicros") { + val s = + """{"type":"long","logicalType":"time-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes logical type timeMicros with default formatter") { + val s = """{"type":"long","logicalType":"time-micros"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalTimeType))) + }, + test("decodes logical type timestampMillis") { + val s = + """{"type":"long","logicalType":"timestamp-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes logical type timestampMillis with default formatter") { + val s = """{"type":"long","logicalType":"timestamp-millis"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes logical type timestampMicros") { + val s = + """{"type":"long","logicalType":"timestamp-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes logical type timestampMicros with default formatter") { + val s = """{"type":"long","logicalType":"timestamp-micros"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.InstantType))) + }, + test("decodes logical type LocalTimestamp millis") { + val s = + """{"type":"long","logicalType":"local-timestamp-millis","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) + }, + test("decodes logical type LocalTimestamp millis with default formatter") { + val s = """{"type":"long","logicalType":"local-timestamp-millis"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isStandardType(StandardType.LocalDateTimeType)) + ) + }, + test("decodes logical type LocalTimestamp micros") { + val s = + """{"type":"long","logicalType":"local-timestamp-micros","zio.schema.codec.avro.dateTimeFormatter":"ISO_ORDINAL_DATE"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.LocalDateTimeType))) + }, + test("decodes logical type LocalTimestamp micros with default formatter") { + val s = """{"type":"long","logicalType":"local-timestamp-micros"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)( + isRight(isStandardType(StandardType.LocalDateTimeType)) + ) + } + ), + test("float") { + val s = """{"type":"float"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.FloatType))) + }, + test("double") { + val s = """{"type":"double"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.DoubleType))) + }, + test("boolean") { + val s = """{"type":"boolean"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.BoolType))) + }, + test("null") { + val s = """{"type":"null"}""" + val schema = AvroSchemaCodec.decode(Chunk.fromArray(s.getBytes())) + + assert(schema)(isRight(isStandardType(StandardType.UnitType))) + } + ), + test("encode/decode full adt test") { + val initialSchemaDerived = DeriveSchema.gen[FullAdtTest.TopLevelUnion] + + val decoded = for { + avroSchemaString <- AvroSchemaCodec.encode(initialSchemaDerived) + decoded <- AvroSchemaCodec.decode(Chunk.fromArray(avroSchemaString.getBytes())) + //_ <- AvroSchemaCodec.encode(decoded) TODO: this fails + } yield decoded + + assert(decoded)(isRight(hasField("ast", _.ast, equalTo(initialSchemaDerived.ast)))) + } @@ TestAspect.ignore // TODO: FIX + ) +} + +object AssertionHelper { + + def isRecord[A](assertion: Assertion[Schema.Record[A]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Record[A]]( + "Record", { + case r: Schema.Record[_] => Try { r.asInstanceOf[Schema.Record[A]] }.toOption + case _ => None + }, + assertion + ) + + def isEmptyRecord[A]: Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema[_]]( + "EmptyRecord", { + case r: CaseClass0[_] => Some(r) + case r @ GenericRecord(_, structure, _) if structure.toChunk.isEmpty => Some(r) + case _ => None + }, + anything + ) + + def isEnum[A](assertion: Assertion[Schema.Enum[A]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Enum[A]]( + "Enum", { + case r: Schema.Enum[_] => Try { r.asInstanceOf[Schema.Enum[A]] }.toOption + case _ => None + }, + assertion + ) + + def isSequence[A](assertion: Assertion[Schema.Sequence[_, A, _]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Sequence[_, A, _]]( + "List", { + case r: Schema.Sequence[_, _, _] => Try { r.asInstanceOf[Schema.Sequence[_, A, _]] }.toOption + case _ => None + }, + assertion + ) + + def isMap[K, V](assertion: Assertion[Schema.Map[K, V]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Map[K, V]]( + "Map", { + case r: Schema.Map[_, _] => Try { r.asInstanceOf[Schema.Map[K, V]] }.toOption + case _ => None + }, + assertion + ) + + def isTuple[A, B](assertion: Assertion[Schema.Tuple2[A, B]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Tuple2[A, B]]( + "Tuple", { + case r: Schema.Tuple2[_, _] => Try { r.asInstanceOf[Schema.Tuple2[A, B]] }.toOption + case _ => None + }, + assertion + ) + + def isTuple[A, B](assertionA: Assertion[Schema[A]], assertionB: Assertion[Schema[B]]): Assertion[Schema[_]] = + isTuple[A, B]( + hasField[Schema.Tuple2[A, B], Schema[A]]("left", _.left, assertionA) && hasField[Schema.Tuple2[A, B], Schema[B]]( + "right", + _.right, + assertionB + ) + ) + + def isEither[A, B](assertion: Assertion[Schema.Either[A, B]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Either[A, B]]( + "Either", { + case r: Schema.Either[_, _] => Try { r.asInstanceOf[Schema.Either[A, B]] }.toOption + case _ => None + }, + assertion + ) + + def isEither[A, B](leftAssertion: Assertion[Schema[A]], rightAssertion: Assertion[Schema[B]]): Assertion[Schema[_]] = + isEither[A, B]( + hasField[Schema.Either[A, B], Schema[A]]("left", _.left, leftAssertion) && hasField[ + Schema.Either[A, B], + Schema[B] + ]("right", _.right, rightAssertion) + ) + + def isOption[A](assertion: Assertion[Schema.Optional[A]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Schema.Optional[A]]( + "Optional", { + case r: Schema.Optional[_] => Try { r.asInstanceOf[Schema.Optional[A]] }.toOption + case _ => None + }, + assertion + ) + + def tuple2First[A](assertion: Assertion[A]): Assertion[(A, _)] = + Assertion.isCase[(A, _), A]("Tuple", { + case (a, _) => Some(a) + }, assertion) + + def hasMapKeys[K](assertion: Assertion[Schema[K]]): Assertion[Schema.Map[K, _]] = + hasField("keySchema", _.keySchema, assertion) + + def hasMapValues[V](assertion: Assertion[Schema[V]]): Assertion[Schema.Map[_, V]] = + hasField("valueSchema", _.valueSchema, assertion) + + def enumStructure(assertion: Assertion[ListMap[String, (Schema[_], Chunk[Any])]]): Assertion[Schema.Enum[_]] = + Assertion.assertionRec("enumStructure")(assertion)( + enum => + Some(`enum`.cases.foldRight(ListMap.empty[String, (Schema[_], Chunk[Any])]) { (caseValue, acc) => + (acc + (caseValue.id -> scala.Tuple2(caseValue.schema, caseValue.annotations))) + }) + ) + + def annotations(assertion: Assertion[Chunk[Any]]): Assertion[Any] = + Assertion.assertionRec("hasAnnotations")(assertion) { + case s: Schema[_] => Some(s.annotations) + case f: Schema.Field[_, _] => Some(f.annotations) + case _ => None + } + + def hasNameAnnotation(assertion: Assertion[String]): Assertion[Any] = + annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.name](hasField("name", _.name, assertion)))) + + def hasNamespaceAnnotation(assertion: Assertion[String]): Assertion[Any] = + annotations( + Assertion.exists(Assertion.isSubtype[AvroAnnotations.namespace](hasField("namespace", _.namespace, assertion))) + ) + + def hasDocAnnotation(assertion: Assertion[String]): Assertion[Any] = + annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.doc](hasField("doc", _.doc, assertion)))) + + def hasFieldOrderAnnotation(assertion: Assertion[FieldOrderType]): Assertion[Field[_, _]] = + annotations( + Assertion.exists( + Assertion.isSubtype[AvroAnnotations.fieldOrder](hasField("fieldOrderType", _.fieldOrderType, assertion)) + ) + ) + + def hasAliasesAnnotation(assertion: Assertion[Iterable[String]]): Assertion[Any] = + annotations( + Assertion.exists(Assertion.isSubtype[AvroAnnotations.aliases](hasField("aliases", _.aliases, assertion))) + ) + + def hasFieldDefaultAnnotation(assertion: Assertion[Object]): Assertion[Field[_, _]] = + annotations( + Assertion.exists( + Assertion.isSubtype[AvroAnnotations.default](hasField("javaDefaultObject", _.javaDefaultObject, assertion)) + ) + ) + + def hasDefaultAnnotation(assertion: Assertion[Object]): Assertion[Schema[_]] = + annotations( + Assertion.exists( + Assertion.isSubtype[AvroAnnotations.default](hasField("javaDefaultObject", _.javaDefaultObject, assertion)) + ) + ) + + val hasErrorAnnotation: Assertion[Any] = + annotations(Assertion.exists(Assertion.isSubtype[AvroAnnotations.error.type](Assertion.anything))) + + def asString(assertion: Assertion[String]): Assertion[Any] = + Assertion.assertionRec("asString")(assertion)(v => Some(v.toString)) + + def recordFields(assertion: Assertion[Iterable[Schema.Field[_, _]]]): Assertion[Schema.Record[_]] = + Assertion.assertionRec[Schema.Record[_], Chunk[Field[_, _]]]("hasRecordField")( + assertion + ) { + case r: Schema.Record[_] => Some(r.fields) + case _ => None + } + + def hasSequenceElementSchema[A](assertion: Assertion[Schema[A]]): Assertion[Schema.Sequence[_, A, _]] = + Assertion.hasField("schemaA", _.elementSchema, assertion) + + def hasOptionElementSchema[A](assertion: Assertion[Schema[A]]): Assertion[Schema.Optional[A]] = + Assertion.hasField("schema", _.schema, assertion) + + def hasRecordField(assertion: Assertion[Schema.Field[_, _]]): Assertion[Schema.Record[_]] = + recordFields(Assertion.exists(assertion)) + + def hasLabel(assertion: Assertion[String]): Assertion[Schema.Field[_, _]] = + hasField("label", _.name, assertion) + + def hasSchema(assertion: Assertion[Schema[_]]): Assertion[Schema.Field[_, _]] = + hasField("initialSchemaDerived", _.schema, assertion) + + def isPrimitive[A](assertion: Assertion[Primitive[A]]): Assertion[Schema[_]] = + Assertion.isCase[Schema[_], Primitive[A]]("Primitive", { + case p: Primitive[_] => Try { p.asInstanceOf[Primitive[A]] }.toOption + case _ => None + }, assertion) + + def isStandardType[A](standardType: StandardType[A]): Assertion[Schema[_]] = + isPrimitive[A](hasField("standardType", _.standardType, equalTo(standardType))) + + def isPrimitiveType[A](assertion: Assertion[StandardType[A]]): Assertion[Schema[_]] = + isPrimitive[A](hasField("standardType", _.standardType, assertion)) +} + +object SpecTestData { + + @AvroAnnotations.name("MyEnum") + sealed trait CaseObjectsOnlyAdt + + object CaseObjectsOnlyAdt { + case object A extends CaseObjectsOnlyAdt + case object B extends CaseObjectsOnlyAdt + + @AvroAnnotations.name("MyC") + case object C extends CaseObjectsOnlyAdt + } + + @AvroAnnotations.avroEnum + sealed trait CaseObjectAndCaseClassAdt + + object CaseObjectAndCaseClassAdt { + case object A extends CaseObjectAndCaseClassAdt + case object B extends CaseObjectAndCaseClassAdt + + @AvroAnnotations.name("MyC") + case object C extends CaseObjectAndCaseClassAdt + case class D(s: String) extends CaseObjectAndCaseClassAdt + } + + sealed trait UnionWithNesting + + object UnionWithNesting { + sealed trait Nested extends UnionWithNesting + + object Nested { + case object A extends Nested + case object B extends Nested + } + + @AvroAnnotations.name("MyC") + case object C extends UnionWithNesting + case class D(s: String) extends UnionWithNesting + } + + case class Record(s: String, b: Boolean) + + @AvroAnnotations.name("MyNamedRecord") + case class NamedRecord(s: String, b: Boolean) + + @AvroAnnotations.name("MyNamedFieldRecord") + case class NamedFieldRecord(@AvroAnnotations.name("myNamedField") s: String, b: Boolean) + + @AvroAnnotations.name("NestedRecord") + case class NestedRecord(s: String, nested: NamedRecord) + + @AvroAnnotations.name("Simple") + case class SimpleRecord(s: String) + + object FullAdtTest { + sealed trait TopLevelUnion + + object TopLevelUnion { + case class RecordWithPrimitives( + string: String, + bool: Boolean, + int: Int, + double: Double, + float: Float, + short: Short, + bigInt: BigInt, + bigDecimal: BigDecimal, + unit: Unit, + char: Char, + uuid: UUID + ) extends TopLevelUnion + case class NestedRecord(innerRecord: InnerRecord) extends TopLevelUnion + case class Unions(union: Union) extends TopLevelUnion + case class Enumeration(`enum`: Enum) extends TopLevelUnion + case class Iterables(list: List[String], map: scala.collection.immutable.Map[String, Int]) extends TopLevelUnion + + // TODO: Schema derivation fails for the following case classes + // case class RecordWithTimeRelatedPrimitives(localDateTime: LocalDateTime, localTime: LocalTime, localDate: LocalDate, offsetTime: OffsetTime, offsetDateTime: OffsetDateTime, zonedDateTime: ZonedDateTime, zoneOffset: ZoneOffset, zoneId: ZoneId, instant: Instant) extends TopLevelUnion + // case class IterablesComplex(list: List[InnerRecord], map: Map[InnerRecord, Enum]) extends TopLevelUnion + } + + case class InnerRecord(s: String, union: Option[scala.util.Either[String, Int]]) + + sealed trait Union + case class NestedUnion(inner: InnerUnion) extends Union + case object OtherCase extends Union + + sealed trait InnerUnion + case class InnerUnionCase1(s: String) extends InnerUnion + case class InnerUnionCase2(i: Int) extends InnerUnion + sealed trait InnerUnionNested extends InnerUnion + case object InnerUnionNestedCase1 extends InnerUnionNested + case object InnerUnionNestedCase2 extends InnerUnionNested + + sealed trait Enum + case object EnumCase1 extends Enum + case object EnumCase2 extends Enum + } +} diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala index 8c22480fa..fb56ec666 100644 --- a/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala @@ -273,6 +273,7 @@ object JsonCodec { case Schema.Primitive(StandardType.StringType, _) => Option(JsonFieldEncoder.string) case Schema.Primitive(StandardType.LongType, _) => Option(JsonFieldEncoder.long) case Schema.Primitive(StandardType.IntType, _) => Option(JsonFieldEncoder.int) + case Schema.Lazy(inner) => jsonFieldEncoder(inner()) case _ => None } @@ -598,6 +599,7 @@ object JsonCodec { case Schema.Primitive(StandardType.StringType, _) => Option(JsonFieldDecoder.string) case Schema.Primitive(StandardType.LongType, _) => Option(JsonFieldDecoder.long) case Schema.Primitive(StandardType.IntType, _) => Option(JsonFieldDecoder.int) + case Schema.Lazy(inner) => jsonFieldDecoder(inner()) case _ => None } diff --git a/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala b/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala new file mode 100644 index 000000000..d1eeb7135 --- /dev/null +++ b/zio-schema-json/shared/src/main/scala/zio/schema/codec/package.scala @@ -0,0 +1,85 @@ +package zio.schema.codec + +import java.util.Base64 + +import scala.collection.immutable.ListMap + +import zio.Chunk +import zio.json.ast.Json +import zio.schema.annotation.directDynamicMapping +import zio.schema.{ DynamicValue, Schema, StandardType, TypeId } + +package object json { + implicit val schemaJson: Schema[Json] = + Schema.dynamicValue.annotate(directDynamicMapping()).transform(toJson, fromJson) + + private def toJson(dv: DynamicValue): Json = + dv match { + case DynamicValue.Record(_, values) => + values.foldLeft(Json.Obj()) { case (obj, (name, value)) => (name, toJson(value)) +: obj } + case DynamicValue.Enumeration(_, _) => + throw new Exception("DynamicValue.Enumeration is unsupported") + case DynamicValue.Sequence(values) => + Json.Arr(values.map(toJson)) + case DynamicValue.Dictionary(_) => + throw new Exception("DynamicValue.Dictionary is unsupported") + case DynamicValue.SetValue(values) => + Json.Arr(Chunk.fromIterable(values.map(toJson))) + case DynamicValue.Primitive(value, standardType) => + standardType.asInstanceOf[StandardType[_]] match { + case StandardType.UnitType => Json.Obj() + case StandardType.StringType => Json.Str(value.asInstanceOf[String]) + case StandardType.BoolType => Json.Bool(value.asInstanceOf[Boolean]) + case StandardType.ByteType => Json.Num(value.asInstanceOf[Byte]) + case StandardType.ShortType => Json.Num(value.asInstanceOf[Short]) + case StandardType.IntType => Json.Num(value.asInstanceOf[Int]) + case StandardType.LongType => Json.Num(value.asInstanceOf[Long]) + case StandardType.FloatType => Json.Num(value.asInstanceOf[Float]) + case StandardType.DoubleType => Json.Num(value.asInstanceOf[Double]) + case StandardType.BinaryType => + Json.Str(Base64.getEncoder.encodeToString(value.asInstanceOf[Chunk[Byte]].toArray)) + case StandardType.CharType => Json.Str(value.asInstanceOf[Char].toString) + case StandardType.UUIDType => Json.Str(value.asInstanceOf[java.util.UUID].toString) + case StandardType.BigDecimalType => Json.Num(value.asInstanceOf[java.math.BigDecimal]) + case StandardType.BigIntegerType => Json.Num(BigDecimal(value.asInstanceOf[java.math.BigInteger])) + case StandardType.DayOfWeekType => Json.Str(value.asInstanceOf[java.time.DayOfWeek].toString) + case StandardType.MonthType => Json.Str(value.asInstanceOf[java.time.Month].toString) + case StandardType.MonthDayType => Json.Str(value.asInstanceOf[java.time.MonthDay].toString) + case StandardType.PeriodType => Json.Str(value.asInstanceOf[java.time.Period].toString) + case StandardType.YearType => Json.Num(value.asInstanceOf[java.time.Year].getValue) + case StandardType.YearMonthType => Json.Str(value.asInstanceOf[java.time.YearMonth].toString) + case StandardType.ZoneIdType => Json.Str(value.asInstanceOf[java.time.ZoneId].toString) + case StandardType.ZoneOffsetType => Json.Str(value.asInstanceOf[java.time.ZoneOffset].toString) + case StandardType.DurationType => Json.Str(value.asInstanceOf[java.time.Duration].toString) + case StandardType.InstantType => Json.Str(value.asInstanceOf[java.time.Instant].toString) + case StandardType.LocalDateType => Json.Str(value.asInstanceOf[java.time.LocalDate].toString) + case StandardType.LocalTimeType => Json.Str(value.asInstanceOf[java.time.LocalTime].toString) + case StandardType.LocalDateTimeType => Json.Str(value.asInstanceOf[java.time.LocalDateTime].toString) + case StandardType.OffsetTimeType => Json.Str(value.asInstanceOf[java.time.OffsetTime].toString) + case StandardType.OffsetDateTimeType => Json.Str(value.asInstanceOf[java.time.OffsetDateTime].toString) + case StandardType.ZonedDateTimeType => Json.Str(value.asInstanceOf[java.time.ZonedDateTime].toString) + } + case DynamicValue.Singleton(_) => Json.Obj() + case DynamicValue.SomeValue(value) => toJson(value) + case DynamicValue.NoneValue => Json.Null + case DynamicValue.Tuple(left, right) => Json.Arr(Chunk(toJson(left), toJson(right))) + case DynamicValue.LeftValue(value) => Json.Obj("Left" -> toJson(value)) + case DynamicValue.RightValue(value) => Json.Obj("Right" -> toJson(value)) + case DynamicValue.DynamicAst(_) => throw new Exception("DynamicValue.DynamicAst is unsupported") + case DynamicValue.Error(_) => throw new Exception("DynamicValue.Error is unsupported") + } + + private def fromJson(json: Json): DynamicValue = + json match { + case Json.Null => DynamicValue.NoneValue + case Json.Bool(value) => DynamicValue.Primitive(value, StandardType.BoolType) + case Json.Num(value) => DynamicValue.Primitive(value, StandardType.BigDecimalType) + case Json.Str(value) => DynamicValue.Primitive(value, StandardType.StringType) + case Json.Arr(values) => DynamicValue.Sequence(values.map(fromJson)) + case Json.Obj(values) => + DynamicValue.Record( + TypeId.parse("Json.Obj"), + ListMap(values.map { case (name, value) => (name, fromJson(value)) }.toList: _*) + ) + } +} diff --git a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala index d35adb72d..b3b66901f 100644 --- a/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala +++ b/zio-schema-json/shared/src/test/scala-2/zio/schema/codec/JsonCodecSpec.scala @@ -7,6 +7,7 @@ import scala.collection.immutable.ListMap import zio.Console._ import zio._ import zio.json.JsonDecoder.JsonError +import zio.json.ast.Json import zio.json.{ DeriveJsonEncoder, JsonEncoder } import zio.schema.CaseSet._ import zio.schema._ @@ -96,6 +97,15 @@ object JsonCodecSpec extends ZIOSpecDefault { """{"0":{"first":0,"second":true},"1":{"first":1,"second":false}}""" ) ) + }, + test("of simple keys and values where the key's schema is lazy") { + assertEncodes( + Schema.map[Int, Value](Schema.defer(Schema[Int]), Schema[Value]), + Map(0 -> Value(0, true), 1 -> Value(1, false)), + charSequenceToByteChunk( + """{"0":{"first":0,"second":true},"1":{"first":1,"second":false}}""" + ) + ) } ), suite("Set")( @@ -242,6 +252,64 @@ object JsonCodecSpec extends ZIOSpecDefault { charSequenceToByteChunk("""{"foo":"s","bar":1}""") ) } + ), + suite("zio.json.ast.Json encoding")( + test("Json.Obj") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Obj("foo" -> Json.Str("bar"), "null" -> Json.Null), + charSequenceToByteChunk("""{"foo":"bar","null":null}""") + ) + }, + test("Json.Arr") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Arr(Json.Str("foo"), Json.Num(1)), + charSequenceToByteChunk("""["foo",1]""") + ) + }, + test("Json.Num Int") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Long") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1L), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Double") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Num(1.1), + charSequenceToByteChunk("""1.1""") + ) + }, + test("Json.Str") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Str("foo"), + charSequenceToByteChunk(""""foo"""") + ) + }, + test("Json.Bool") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Bool(true), + charSequenceToByteChunk("""true""") + ) + }, + test("Json.Null") { + assertEncodes( + zio.schema.codec.json.schemaJson, + Json.Null, + charSequenceToByteChunk("""null""") + ) + } ) ) @@ -469,6 +537,73 @@ object JsonCodecSpec extends ZIOSpecDefault { """{"0":{"first":0,"second":true},"1":{"first":1,"second":false}}""" ) ) + }, + test("of simple keys and values where the key schema is lazy") { + assertDecodes( + Schema.map[Int, Value](Schema.defer(Schema[Int]), Schema[Value]), + Map(0 -> Value(0, true), 1 -> Value(1, false)), + charSequenceToByteChunk( + """{"0":{"first":0,"second":true},"1":{"first":1,"second":false}}""" + ) + ) + } + ), + suite("zio.json.ast.Json decoding")( + test("Json.Obj") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Obj("foo" -> Json.Str("bar"), "null" -> Json.Null), + charSequenceToByteChunk("""{"foo":"bar","null":null}""") + ) + }, + test("Json.Arr") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Arr(Json.Str("foo"), Json.Num(1)), + charSequenceToByteChunk("""["foo",1]""") + ) + }, + test("Json.Num Int") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Long") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1L), + charSequenceToByteChunk("""1""") + ) + }, + test("Json.Num Double") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Num(1.1), + charSequenceToByteChunk("""1.1""") + ) + }, + test("Json.Str") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Str("foo"), + charSequenceToByteChunk(""""foo"""") + ) + }, + test("Json.Bool") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Bool(true), + charSequenceToByteChunk("""true""") + ) + }, + test("Json.Null") { + assertDecodes( + zio.schema.codec.json.schemaJson, + Json.Null, + charSequenceToByteChunk("""null""") + ) } ) )