From 80628bc8b5ac6cc7cfd40501884aed2acc68a24a Mon Sep 17 00:00:00 2001 From: Marco Lopes Date: Mon, 3 Jun 2019 20:48:40 +0100 Subject: [PATCH 1/2] Add avro4s encoder for DateTime --- build.sbt | 12 ++++++++- .../src/main/scala/wen/avro4s/package.scala | 23 ++++++++++++++++ .../test/scala/wen/avro4s/Avro4sSpec.scala | 26 +++++++++++++++++++ .../src/test/scala/wen/test/Generators.scala | 3 +++ project/Dependencies.scala | 5 ++++ 5 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 modules/avro4s/src/main/scala/wen/avro4s/package.scala create mode 100644 modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala 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..00fe787 --- /dev/null +++ b/modules/avro4s/src/main/scala/wen/avro4s/package.scala @@ -0,0 +1,23 @@ +package wen + +import java.time.Instant + +import cats.implicits._ +import com.sksamuel.avro4s.{Encoder, SchemaFor} +import com.sksamuel.avro4s.Encoder.LongEncoder +import org.apache.avro.Schema +import wen.datetime.DateTime +import wen.instances.iso._ + +package object avro4s { + implicit object LocalDateTimeSchemaFor 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 + } + } +} \ 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..f5d0ace --- /dev/null +++ b/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala @@ -0,0 +1,26 @@ +package wen.avro4s + +import java.time.{Instant, LocalDateTime, 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 +import wen.test.Generators._ + +class Avro4sSpec extends WordSpec with Matchers with TypeCheckedTripleEquals with ScalaCheckDrivenPropertyChecks { + + "Avro4s Encoders" should { + "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..0a1f7c7 100644 --- a/modules/core/src/test/scala/wen/test/Generators.scala +++ b/modules/core/src/test/scala/wen/test/Generators.scala @@ -77,4 +77,7 @@ 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) + } 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 ) From c19c31047f47bf22a388ebe7fb3d13bdbd98eba1 Mon Sep 17 00:00:00 2001 From: Marco Lopes Date: Tue, 4 Jun 2019 19:25:43 +0100 Subject: [PATCH 2/2] Add avro4s encoder for Time --- .../src/main/scala/wen/avro4s/package.scala | 25 ++++++++++++++++--- .../test/scala/wen/avro4s/Avro4sSpec.scala | 14 +++++++++-- .../src/test/scala/wen/test/Generators.scala | 3 +++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/modules/avro4s/src/main/scala/wen/avro4s/package.scala b/modules/avro4s/src/main/scala/wen/avro4s/package.scala index 00fe787..bb11ec4 100644 --- a/modules/avro4s/src/main/scala/wen/avro4s/package.scala +++ b/modules/avro4s/src/main/scala/wen/avro4s/package.scala @@ -4,20 +4,37 @@ import java.time.Instant import cats.implicits._ import com.sksamuel.avro4s.{Encoder, SchemaFor} -import com.sksamuel.avro4s.Encoder.LongEncoder +import com.sksamuel.avro4s.Encoder._ import org.apache.avro.Schema -import wen.datetime.DateTime +import wen.datetime._ import wen.instances.iso._ +import wen.types._ package object avro4s { - implicit object LocalDateTimeSchemaFor extends SchemaFor[DateTime] { + implicit object DateTimeSchemaFor extends SchemaFor[DateTime] { override def schema: Schema = Schema.create(Schema.Type.LONG) } implicit val DateTimeEncoder: Encoder[DateTime] = { - LongEncoder.comap[DateTime]{ x => + 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 index f5d0ace..8a963a0 100644 --- a/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala +++ b/modules/avro4s/src/test/scala/wen/avro4s/Avro4sSpec.scala @@ -1,17 +1,27 @@ package wen.avro4s -import java.time.{Instant, LocalDateTime, ZoneOffset} +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 +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) diff --git a/modules/core/src/test/scala/wen/test/Generators.scala b/modules/core/src/test/scala/wen/test/Generators.scala index 0a1f7c7..fce2bf1 100644 --- a/modules/core/src/test/scala/wen/test/Generators.scala +++ b/modules/core/src/test/scala/wen/test/Generators.scala @@ -80,4 +80,7 @@ object Generators { val epochLong: Gen[Long] = Gen.choose(0, Long.MaxValue) + val timeOfDayInMilliseconds: Gen[Int] = + Gen.choose(0, 84239999) + }