diff --git a/build.sbt b/build.sbt index 5aec235..eff7434 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ ThisBuild / organizationName := "mlopes" lazy val wen = project .in(file(".")) - .aggregate(core, cats, circe) + .aggregate(core, cats, circe, avro4s) .settings(name := "Wen Root") .settings( publish := {}, @@ -42,6 +42,16 @@ lazy val circe = project defaultConfig ) +lazy val avro4s = project + .in(file("modules/avro4s")) + .dependsOn(core % "compile->compile;test->test", cats) + .settings(moduleName := "wen-avro4s", name := "Wen Avro4s", description := "Avro4s instances for Wen") + .settings( + libraryDependencies ++= avro4sDependencies, + libraryDependencies ++= testDependencies, + defaultConfig + ) + lazy val defaultConfig = Seq( scalacOptions := appScalacOptions, compile in Compile := (compile in Compile).dependsOn(dependencyUpdates).value, diff --git a/modules/avro4s/src/main/scala/wen/avro4s/package.scala b/modules/avro4s/src/main/scala/wen/avro4s/package.scala new file mode 100644 index 0000000..bb11ec4 --- /dev/null +++ b/modules/avro4s/src/main/scala/wen/avro4s/package.scala @@ -0,0 +1,40 @@ +package wen + +import java.time.Instant + +import cats.implicits._ +import com.sksamuel.avro4s.{Encoder, SchemaFor} +import com.sksamuel.avro4s.Encoder._ +import org.apache.avro.Schema +import wen.datetime._ +import wen.instances.iso._ +import wen.types._ + +package object avro4s { + implicit object DateTimeSchemaFor extends SchemaFor[DateTime] { + override def schema: Schema = Schema.create(Schema.Type.LONG) + } + + implicit val DateTimeEncoder: Encoder[DateTime] = { + LongEncoder.comap[DateTime] { x => + val dateTimeAsString: String = f"${x.show}.${x.time.millisecond.millisecond.value}%03dZ" + Instant.parse(dateTimeAsString).toEpochMilli + } + } + + implicit object TimeSchemaFor extends SchemaFor[Time] { + override def schema: Schema = Schema.create(Schema.Type.INT) + } + + implicit val TimeEncoder: Encoder[Time] = { + IntEncoder.comap[Time] { x => + x match { + case Time(Hour(h), Minute(m), Second(s), Millisecond(ms)) => + (h.value * 60 * 60 * 1000) + + (m.value * 60 * 1000) + + (s.value * 1000) + + ms.value + } + } + } +} \ No newline at end of file diff --git a/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala b/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala new file mode 100644 index 0000000..8a963a0 --- /dev/null +++ b/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala @@ -0,0 +1,36 @@ +package wen.avro4s + +import java.time.{Instant, LocalDateTime, LocalTime, ZoneOffset} + +import com.sksamuel.avro4s.{AvroSchema, Encoder, ImmutableRecord} +import org.scalactic.TypeCheckedTripleEquals +import org.scalatest.{Matchers, WordSpec} +import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import wen.datetime.{DateTime, Time} +import wen.test.Generators._ + +class Avro4sSpec extends WordSpec with Matchers with TypeCheckedTripleEquals with ScalaCheckDrivenPropertyChecks { + + "Avro4s Encoders" should { + "encode a Time" in forAll (timeOfDayInMilliseconds) { millisecondsOfDay: Int => + case class Foo(s: Time) + + val schema = AvroSchema[Foo] + val localTime = LocalTime.ofNanoOfDay(millisecondsOfDay * 1000000L) + val time = Time(localTime) + + Encoder[Foo].encode(Foo(time), schema) shouldBe ImmutableRecord(schema, Vector(java.lang.Integer.valueOf(millisecondsOfDay))) + } + + "encode a DateTime" in forAll (epochLong) { timestamp: Long => + case class Foo(s: DateTime) + + val schema = AvroSchema[Foo] + val localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) + val dateTime = DateTime(localDateTime) + + Encoder[Foo].encode(Foo(dateTime), schema) shouldBe ImmutableRecord(schema, Vector(java.lang.Long.valueOf(timestamp))) + } + } + +} diff --git a/modules/core/src/test/scala/wen/test/Generators.scala b/modules/core/src/test/scala/wen/test/Generators.scala index c9036e7..fce2bf1 100644 --- a/modules/core/src/test/scala/wen/test/Generators.scala +++ b/modules/core/src/test/scala/wen/test/Generators.scala @@ -77,4 +77,10 @@ object Generators { val yearWithDefaultEpochGen: Gen[Year] = Gen.posNum[Int].map(i => Year(refineV[NumericYearConstraint].unsafeFrom(i))) + val epochLong: Gen[Long] = + Gen.choose(0, Long.MaxValue) + + val timeOfDayInMilliseconds: Gen[Int] = + Gen.choose(0, 84239999) + } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 729585a..4179778 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,6 +7,7 @@ object Dependencies { val refinedVersion = "0.9.5" val catsVersion = "1.6.0" val circeVersion = "0.11.1" + val avro4sVersion = "2.0.4" lazy val testDependencies = Seq( @@ -20,6 +21,10 @@ object Dependencies { "org.typelevel" %% "cats-core" % catsVersion ) + lazy val avro4sDependencies = Seq( + "com.sksamuel.avro4s" %% "avro4s-core" % avro4sVersion + ) + lazy val refinedDependencies = Seq( "eu.timepit" %% "refined" % refinedVersion )