diff --git a/avro/src/main/scala/magnolify/avro/AvroType.scala b/avro/src/main/scala/magnolify/avro/AvroType.scala index 68697c832..46549329a 100644 --- a/avro/src/main/scala/magnolify/avro/AvroType.scala +++ b/avro/src/main/scala/magnolify/avro/AvroType.scala @@ -16,21 +16,22 @@ package magnolify.avro -import java.nio.ByteBuffer -import java.time._ -import java.{util => ju} import magnolia1._ import magnolify.shared._ import magnolify.shims.FactoryCompat import org.apache.avro.generic.GenericData.EnumSymbol import org.apache.avro.generic._ import org.apache.avro.{JsonProperties, LogicalType, LogicalTypes, Schema} +import org.joda.{time => joda} +import java.nio.{ByteBuffer, ByteOrder} +import java.time._ +import java.{util => ju} import scala.annotation.{implicitNotFound, nowarn} import scala.collection.concurrent -import scala.reflect.ClassTag -import scala.jdk.CollectionConverters._ import scala.collection.compat._ +import scala.jdk.CollectionConverters._ +import scala.reflect.ClassTag sealed trait AvroType[T] extends Converter[T, GenericRecord, GenericRecord] { val schema: Schema @@ -200,14 +201,14 @@ object AvroField { implicit val afUnit = aux2[Unit, JsonProperties.Null](Schema.Type.NULL)(_ => ())(_ => JsonProperties.NULL_VALUE) - implicit val afBytes = new Aux[Array[Byte], ByteBuffer, ByteBuffer] { + implicit val afByteBuffer: AvroField[ByteBuffer] = new Aux[ByteBuffer, ByteBuffer, ByteBuffer] { override protected def buildSchema(cm: CaseMapper): Schema = Schema.create(Schema.Type.BYTES) // `JacksonUtils.toJson` expects `Array[Byte]` for `BYTES` defaults - override def makeDefault(d: Array[Byte])(cm: CaseMapper): Array[Byte] = d - override def from(v: ByteBuffer)(cm: CaseMapper): Array[Byte] = - ju.Arrays.copyOfRange(v.array(), v.position(), v.limit()) - override def to(v: Array[Byte])(cm: CaseMapper): ByteBuffer = ByteBuffer.wrap(v) + override def makeDefault(d: ByteBuffer)(cm: CaseMapper): Array[Byte] = d.array() + override def from(v: ByteBuffer)(cm: CaseMapper): ByteBuffer = v + override def to(v: ByteBuffer)(cm: CaseMapper): ByteBuffer = v } + implicit val afBytes: AvroField[Array[Byte]] = from[ByteBuffer](_.array())(ByteBuffer.wrap) @nowarn("msg=parameter value lp in method afEnum is never used") implicit def afEnum[T](implicit et: EnumType[T], lp: shapeless.LowPriority): AvroField[T] = @@ -295,6 +296,33 @@ object AvroField { logicalType[String](LogicalTypes.uuid())(ju.UUID.fromString)(_.toString) implicit val afDate: AvroField[LocalDate] = logicalType[Int](LogicalTypes.date())(x => LocalDate.ofEpochDay(x.toLong))(_.toEpochDay.toInt) + private lazy val EpochJodaDate = new joda.LocalDate(1970, 1, 1) + implicit val afJodaDate: AvroField[joda.LocalDate] = + logicalType[Int](LogicalTypes.date()) { daysFromEpoch => + EpochJodaDate.plusDays(daysFromEpoch) + } { date => + joda.Days.daysBetween(EpochJodaDate, date).getDays + } + + // duration, as in the avro spec. do not make implicit as there is not a specific type for it + // A duration logical type annotates Avro fixed type of size 12, which stores three little-endian unsigned integers + // that represent durations at different granularities of time. + // The first stores a number in months, the second stores a number in days, and the third stores a number in milliseconds. + val afDuration: AvroField[(Long, Long, Long)] = + logicalType[ByteBuffer](new LogicalType("duration")) { bs => + bs.order(ByteOrder.LITTLE_ENDIAN) + val months = java.lang.Integer.toUnsignedLong(bs.getInt) + val days = java.lang.Integer.toUnsignedLong(bs.getInt) + val millis = java.lang.Integer.toUnsignedLong(bs.getInt) + (months, days, millis) + } { case (months, days, millis) => + ByteBuffer + .allocate(12) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(months.toInt) + .putInt(days.toInt) + .putInt(millis.toInt) + }(AvroField.fixed(12)(ByteBuffer.wrap)(_.array())) def fixed[T: ClassTag]( size: Int diff --git a/avro/src/main/scala/magnolify/avro/logical/package.scala b/avro/src/main/scala/magnolify/avro/logical/package.scala index 1ffcb93ab..8abe84a47 100644 --- a/avro/src/main/scala/magnolify/avro/logical/package.scala +++ b/avro/src/main/scala/magnolify/avro/logical/package.scala @@ -17,53 +17,121 @@ package magnolify.avro import org.apache.avro.LogicalTypes.LogicalTypeFactory +import org.apache.avro.{LogicalType, LogicalTypes, Schema} +import org.joda.{time => joda} -import java.time.{Instant, LocalDateTime, LocalTime, ZoneOffset} +import java.time._ import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder} -import org.apache.avro.{LogicalType, LogicalTypes, Schema} +import java.util.concurrent.TimeUnit package object logical { + // Duplicate implementation from org.apache.avro.data.TimeConversions + // to support both 1.8 (joda-time based) and 1.9+ (java-time based) object micros { + private def toTimestampMicros(microsFromEpoch: Long): Instant = { + val epochSeconds = microsFromEpoch / 1000000L + val nanoAdjustment = (microsFromEpoch % 1000000L) * 1000L; + Instant.ofEpochSecond(epochSeconds, nanoAdjustment) + } + + private def fromTimestampMicros(instant: Instant): Long = { + val seconds = instant.getEpochSecond + val nanos = instant.getNano + if (seconds < 0 && nanos > 0) { + val micros = Math.multiplyExact(seconds + 1, 1000000L) + val adjustment = (nanos / 1000L) - 1000000 + Math.addExact(micros, adjustment) + } else { + val micros = Math.multiplyExact(seconds, 1000000L) + Math.addExact(micros, nanos / 1000L) + } + } + implicit val afTimestampMicros: AvroField[Instant] = - AvroField.logicalType[Long](LogicalTypes.timestampMicros())(us => - Instant.ofEpochMilli(us / 1000) - )(_.toEpochMilli * 1000) + AvroField.logicalType[Long](LogicalTypes.timestampMicros())(toTimestampMicros)( + fromTimestampMicros + ) implicit val afTimeMicros: AvroField[LocalTime] = - AvroField.logicalType[Long](LogicalTypes.timeMicros())(us => - LocalTime.ofNanoOfDay(us * 1000) - )(_.toNanoOfDay / 1000) + AvroField.logicalType[Long](LogicalTypes.timeMicros()) { us => + LocalTime.ofNanoOfDay(TimeUnit.MICROSECONDS.toNanos(us)) + } { time => + TimeUnit.NANOSECONDS.toMicros(time.toNanoOfDay) + } - // `LogicalTypes.localTimestampMicros` is Avro 1.10.0+ + // `LogicalTypes.localTimestampMicros()` is Avro 1.10 implicit val afLocalTimestampMicros: AvroField[LocalDateTime] = - AvroField.logicalType[Long](new LogicalType("local-timestamp-micros"))(us => - LocalDateTime.ofInstant(Instant.ofEpochMilli(us / 1000), ZoneOffset.UTC) - )(_.toInstant(ZoneOffset.UTC).toEpochMilli * 1000) + AvroField.logicalType[Long](new LogicalType("local-timestamp-micros")) { microsFromEpoch => + val instant = toTimestampMicros(microsFromEpoch) + LocalDateTime.ofInstant(instant, ZoneOffset.UTC) + } { timestamp => + val instant = timestamp.toInstant(ZoneOffset.UTC) + fromTimestampMicros(instant) + } + + // avro 1.8 uses joda-time + implicit val afJodaTimestampMicros: AvroField[joda.DateTime] = + AvroField.logicalType[Long](LogicalTypes.timestampMicros()) { microsFromEpoch => + new joda.DateTime(microsFromEpoch / 1000, joda.DateTimeZone.UTC) + } { timestamp => + 1000 * timestamp.getMillis + } + + implicit val afJodaTimeMicros: AvroField[joda.LocalTime] = + AvroField.logicalType[Long](LogicalTypes.timeMicros()) { microsFromMidnight => + joda.LocalTime.fromMillisOfDay(microsFromMidnight / 1000) + } { time => + // from LossyTimeMicrosConversion + 1000L * time.millisOfDay().get() + } } object millis { implicit val afTimestampMillis: AvroField[Instant] = - AvroField.logicalType[Long](LogicalTypes.timestampMillis())(Instant.ofEpochMilli)( - _.toEpochMilli - ) + AvroField.logicalType[Long](LogicalTypes.timestampMillis()) { millisFromEpoch => + Instant.ofEpochMilli(millisFromEpoch) + } { timestamp => + timestamp.toEpochMilli + } implicit val afTimeMillis: AvroField[LocalTime] = - AvroField.logicalType[Int](LogicalTypes.timeMillis())(ms => - LocalTime.ofNanoOfDay(ms * 1000000L) - )(t => (t.toNanoOfDay / 1000000).toInt) + AvroField.logicalType[Int](LogicalTypes.timeMillis()) { millisFromMidnight => + LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(millisFromMidnight.toLong)) + } { time => + TimeUnit.NANOSECONDS.toMillis(time.toNanoOfDay).toInt + } // `LogicalTypes.localTimestampMillis` is Avro 1.10.0+ implicit val afLocalTimestampMillis: AvroField[LocalDateTime] = - AvroField.logicalType[Long](new LogicalType("local-timestamp-millis"))(ms => - LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneOffset.UTC) - )(_.toInstant(ZoneOffset.UTC).toEpochMilli) + AvroField.logicalType[Long](new LogicalType("local-timestamp-millis")) { millisFromEpoch => + val instant = Instant.ofEpochMilli(millisFromEpoch) + LocalDateTime.ofInstant(instant, ZoneOffset.UTC) + } { timestamp => + val instant = timestamp.toInstant(ZoneOffset.UTC) + instant.toEpochMilli + } + + // avro 1.8 uses joda-time + implicit val afJodaTimestampMillis: AvroField[joda.DateTime] = + AvroField.logicalType[Long](LogicalTypes.timestampMillis()) { millisFromEpoch => + new joda.DateTime(millisFromEpoch, joda.DateTimeZone.UTC) + } { timestamp => + timestamp.getMillis + } + + implicit val afJodaTimeMillis: AvroField[joda.LocalTime] = + AvroField.logicalType[Int](LogicalTypes.timeMillis()) { millisFromMidnight => + joda.LocalTime.fromMillisOfDay(millisFromMidnight.toLong) + } { time => + time.millisOfDay().get() + } } object bigquery { // datetime is a custom logical type and must be registered private final val DateTimeTypeName = "datetime" private final val DateTimeLogicalTypeFactory: LogicalTypeFactory = (_: Schema) => - new org.apache.avro.LogicalType(DateTimeTypeName) + new LogicalType(DateTimeTypeName) /** * Register custom logical types with avro, which is necessary to correctly parse a custom @@ -72,38 +140,80 @@ package object logical { * on the type name. */ def registerLogicalTypes(): Unit = - org.apache.avro.LogicalTypes.register(DateTimeTypeName, DateTimeLogicalTypeFactory) + LogicalTypes.register(DateTimeTypeName, DateTimeLogicalTypeFactory) // DATETIME // YYYY-[M]M-[D]D[ [H]H:[M]M:[S]S[.DDDDDD]] - private val DatetimePrinter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS") + private val DatePattern = "yyyy-MM-dd" + private val TimePattern = "HH:mm:ss" + private val DecimalPattern = "SSSSSS" + private val DatetimePattern = s"$DatePattern $TimePattern.$DecimalPattern" + private val DatetimePrinter = DateTimeFormatter.ofPattern(DatetimePattern) private val DatetimeParser = new DateTimeFormatterBuilder() - .append(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + .appendPattern(DatePattern) .appendOptional( new DateTimeFormatterBuilder() - .append(DateTimeFormatter.ofPattern(" HH:mm:ss")) - .appendOptional(DateTimeFormatter.ofPattern(".SSSSSS")) + .appendLiteral(' ') + .append(new DateTimeFormatterBuilder().appendPattern(TimePattern).toFormatter) + .appendOptional( + new DateTimeFormatterBuilder() + .appendLiteral('.') + .appendPattern(DecimalPattern) + .toFormatter + ) .toFormatter ) .toFormatter .withZone(ZoneOffset.UTC) + private val JodaDatetimePrinter = new joda.format.DateTimeFormatterBuilder() + .appendPattern(DatetimePattern) + .toFormatter + + private val JodaDatetimeParser = new joda.format.DateTimeFormatterBuilder() + .appendPattern(DatePattern) + .appendOptional( + new joda.format.DateTimeFormatterBuilder() + .appendLiteral(' ') + .appendPattern(TimePattern) + .appendOptional( + new joda.format.DateTimeFormatterBuilder() + .appendLiteral('.') + .appendPattern(DecimalPattern) + .toParser + ) + .toParser + ) + .toFormatter + .withZone(joda.DateTimeZone.UTC) + // NUMERIC // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#numeric-type implicit val afBigQueryNumeric: AvroField[BigDecimal] = AvroField.bigDecimal(38, 9) // TIMESTAMP implicit val afBigQueryTimestamp: AvroField[Instant] = micros.afTimestampMicros + implicit val afBigQueryJodaTimestamp: AvroField[joda.DateTime] = + micros.afJodaTimestampMicros // DATE: `AvroField.afDate` // TIME implicit val afBigQueryTime: AvroField[LocalTime] = micros.afTimeMicros + implicit val afBigQueryJodaTime: AvroField[joda.LocalTime] = micros.afJodaTimeMicros // DATETIME -> sqlType: DATETIME implicit val afBigQueryDatetime: AvroField[LocalDateTime] = - AvroField.logicalType[String](new org.apache.avro.LogicalType(DateTimeTypeName))(s => - LocalDateTime.from(DatetimeParser.parse(s)) - )(DatetimePrinter.format) + AvroField.logicalType[String](new org.apache.avro.LogicalType(DateTimeTypeName)) { str => + LocalDateTime.parse(str, DatetimeParser) + } { datetime => + DatetimePrinter.format(datetime) + } + implicit val afBigQueryJodaDatetime: AvroField[joda.LocalDateTime] = + AvroField.logicalType[String](new org.apache.avro.LogicalType(DateTimeTypeName)) { str => + joda.LocalDateTime.parse(str, JodaDatetimeParser) + } { datetime => + JodaDatetimePrinter.print(datetime) + } } } diff --git a/avro/src/test/scala/magnolify/avro/AvroTypeSuite.scala b/avro/src/test/scala/magnolify/avro/AvroTypeSuite.scala index 2561c38f3..468fab8fd 100644 --- a/avro/src/test/scala/magnolify/avro/AvroTypeSuite.scala +++ b/avro/src/test/scala/magnolify/avro/AvroTypeSuite.scala @@ -18,38 +18,30 @@ package magnolify.avro import cats._ import com.fasterxml.jackson.core.JsonFactory -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import magnolify.avro._ +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import magnolify.avro.unsafe._ -import magnolify.cats.auto._ import magnolify.cats.TestEq._ -import magnolify.scalacheck.auto._ +import magnolify.cats.auto._ import magnolify.scalacheck.TestArbitrary._ +import magnolify.scalacheck.auto._ import magnolify.shared.CaseMapper import magnolify.shared.TestEnumType._ import magnolify.test.Simple._ import magnolify.test._ import org.apache.avro.Schema -import org.apache.avro.generic.GenericDatumReader -import org.apache.avro.generic.GenericDatumWriter -import org.apache.avro.generic.GenericRecord -import org.apache.avro.generic.GenericRecordBuilder -import org.apache.avro.io.DecoderFactory -import org.apache.avro.io.EncoderFactory +import org.apache.avro.generic._ +import org.apache.avro.io.{DecoderFactory, EncoderFactory} +import org.apache.avro.util.Utf8 +import org.joda.{time => joda} import org.scalacheck._ -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream +import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.net.URI import java.nio.ByteBuffer -import java.time.Duration -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime +import java.time._ import java.time.format.DateTimeFormatter -import java.util.UUID +import java.util +import java.util.{Objects, UUID} import scala.jdk.CollectionConverters._ import scala.reflect._ import scala.util.Try @@ -153,7 +145,29 @@ class AvroTypeSuite extends MagnolifySuite { test[MapNested] } - test[Logical] + { + // generate unsigned-int duration values + implicit val arbAvroDuration: Arbitrary[AvroDuration] = Arbitrary { + for { + months <- Gen.chooseNum(0L, 0xffffffffL) + days <- Gen.chooseNum(0L, 0xffffffffL) + millis <- Gen.chooseNum(0L, 0xffffffffL) + } yield AvroDuration(months, days, millis) + } + + implicit val afAvroDuration: AvroField[AvroDuration] = AvroField.from[(Long, Long, Long)] { + case (months, days, millis) => AvroDuration(months, days, millis) + }(d => (d.months, d.days, d.millis))(AvroField.afDuration) + + test[Logical] + + test("LogicalTypes") { + val schema = AvroType[Logical].schema + assertLogicalType(schema, "ld", "date") + assertLogicalType(schema, "jld", "date") + assertLogicalType(schema, "d", "duration") + } + } { import magnolify.avro.logical.micros._ @@ -162,8 +176,10 @@ class AvroTypeSuite extends MagnolifySuite { test("MicrosLogicalTypes") { val schema = AvroType[LogicalMicros].schema assertLogicalType(schema, "i", "timestamp-micros") - assertLogicalType(schema, "dt", "local-timestamp-micros", false) - assertLogicalType(schema, "t", "time-micros") + assertLogicalType(schema, "ldt", "local-timestamp-micros", false) + assertLogicalType(schema, "lt", "time-micros") + assertLogicalType(schema, "jdt", "timestamp-micros") + assertLogicalType(schema, "jlt", "time-micros") } } @@ -174,8 +190,10 @@ class AvroTypeSuite extends MagnolifySuite { test("MilliLogicalTypes") { val schema = AvroType[LogicalMillis].schema assertLogicalType(schema, "i", "timestamp-millis") - assertLogicalType(schema, "dt", "local-timestamp-millis", false) - assertLogicalType(schema, "t", "time-millis") + assertLogicalType(schema, "ldt", "local-timestamp-millis", false) + assertLogicalType(schema, "lt", "time-millis") + assertLogicalType(schema, "jdt", "timestamp-millis") + assertLogicalType(schema, "jlt", "time-millis") } } @@ -192,8 +210,11 @@ class AvroTypeSuite extends MagnolifySuite { val schema = AvroType[LogicalBigQuery].schema assertLogicalType(schema, "bd", "decimal") assertLogicalType(schema, "i", "timestamp-micros") - assertLogicalType(schema, "dt", "datetime") - assertLogicalType(schema, "t", "time-micros") + assertLogicalType(schema, "lt", "time-micros") + assertLogicalType(schema, "ldt", "datetime") + assertLogicalType(schema, "jdt", "timestamp-micros") + assertLogicalType(schema, "jlt", "time-micros") + assertLogicalType(schema, "jldt", "datetime") } } @@ -282,7 +303,12 @@ class AvroTypeSuite extends MagnolifySuite { } } - testFail(AvroType[SomeDefault])("Option[T] can only default to None") + test("DefaultBytes") { + val at = ensureSerializable(AvroType[DefaultBytes]) + assertEquals(at(new GenericRecordBuilder(at.schema).build()), DefaultBytes()) + } + + testFail(AvroType[DefaultSome])("Option[T] can only default to None") { implicit val at: AvroType[LowerCamel] = AvroType[LowerCamel](CaseMapper(_.toUpperCase)) @@ -333,11 +359,31 @@ case class Unsafe(b: Byte, c: Char, s: Short) case class AvroTypes(ba: Array[Byte], u: Unit) case class MapPrimitive(m: Map[String, Int]) case class MapNested(m: Map[String, Nested]) - -case class Logical(u: UUID, d: LocalDate) -case class LogicalMicros(i: Instant, t: LocalTime, dt: LocalDateTime) -case class LogicalMillis(i: Instant, t: LocalTime, dt: LocalDateTime) -case class LogicalBigQuery(bd: BigDecimal, i: Instant, t: LocalTime, dt: LocalDateTime) +case class AvroDuration(months: Long, days: Long, millis: Long) +case class Logical(u: UUID, ld: LocalDate, jld: joda.LocalDate, d: AvroDuration) +case class LogicalMicros( + i: Instant, + lt: LocalTime, + ldt: LocalDateTime, + jdt: joda.DateTime, + jlt: joda.LocalTime +) +case class LogicalMillis( + i: Instant, + lt: LocalTime, + ldt: LocalDateTime, + jdt: joda.DateTime, + jlt: joda.LocalTime +) +case class LogicalBigQuery( + bd: BigDecimal, + i: Instant, + lt: LocalTime, + ldt: LocalDateTime, + jdt: joda.DateTime, + jlt: joda.LocalTime, + jldt: joda.LocalDateTime +) case class BigDec(bd: BigDecimal) @doc("Fixed with doc") @@ -375,9 +421,9 @@ case class DefaultInner( bd: BigDecimal = BigDecimal(111.111), u: UUID = UUID.fromString("11112222-abcd-abcd-abcd-111122223333"), ts: Instant = Instant.ofEpochSecond(11223344), - d: LocalDate = LocalDate.ofEpochDay(1122), - t: LocalTime = LocalTime.of(1, 2, 3), - dt: LocalDateTime = LocalDateTime.of(2001, 2, 3, 4, 5, 6) + ld: LocalDate = LocalDate.ofEpochDay(1122), + lt: LocalTime = LocalTime.of(1, 2, 3), + ldt: LocalDateTime = LocalDateTime.of(2001, 2, 3, 4, 5, 6) ) case class DefaultOuter( i: DefaultInner = DefaultInner( @@ -396,7 +442,23 @@ case class DefaultOuter( ), o: Option[DefaultInner] = None ) -case class SomeDefault(o: Option[Int] = Some(1)) +case class DefaultSome(o: Option[Int] = Some(1)) + +case class DefaultBytes( + a: Array[Byte] = Array(2, 2) + // ByteBuffer is not serializable and can't be used as default value + // bb: ByteBuffer = ByteBuffer.allocate(2).put(2.toByte).put(2.toByte) +) { + + override def hashCode(): Int = + util.Arrays.hashCode(a) + + override def equals(obj: Any): Boolean = obj match { + case that: DefaultBytes => Objects.deepEquals(this.a, that.a) + case _ => false + } + +} @doc("Avro enum") object Pet extends Enumeration { diff --git a/build.sbt b/build.sbt index 102b6f213..2ba569105 100644 --- a/build.sbt +++ b/build.sbt @@ -30,6 +30,7 @@ val datastoreVersion = "2.11.5" val guavaVersion = "31.1-jre" val hadoopVersion = "3.3.4" val jacksonVersion = "2.13.4.2" +val jodaTimeVersion = "2.12.5" val munitVersion = "0.7.29" val neo4jDriverVersion = "4.4.9" val paigesVersion = "0.4.2" @@ -257,7 +258,10 @@ lazy val scalacheck: Project = project commonSettings, moduleName := "magnolify-scalacheck", description := "Magnolia add-on for ScalaCheck", - libraryDependencies += "org.scalacheck" %% "scalacheck" % scalacheckVersion % Provided + libraryDependencies ++= Seq( + "org.scalacheck" %% "scalacheck" % scalacheckVersion % Provided, + "joda-time" % "joda-time" % jodaTimeVersion % Test + ) ) .dependsOn( shared, @@ -272,6 +276,7 @@ lazy val cats: Project = project description := "Magnolia add-on for Cats", libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % catsVersion % Provided, + "joda-time" % "joda-time" % jodaTimeVersion % Test, "com.twitter" %% "algebird-core" % algebirdVersion % Test, "org.typelevel" %% "cats-laws" % catsVersion % Test ) @@ -333,6 +338,7 @@ lazy val avro: Project = project moduleName := "magnolify-avro", description := "Magnolia add-on for Apache Avro", libraryDependencies ++= Seq( + "joda-time" % "joda-time" % jodaTimeVersion % Provided, "org.apache.avro" % "avro" % avroVersion % Provided, "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion % Test ) diff --git a/cats/src/test/scala/magnolify/cats/TestEq.scala b/cats/src/test/scala/magnolify/cats/TestEq.scala index ac45ec425..23ec47e8c 100644 --- a/cats/src/test/scala/magnolify/cats/TestEq.scala +++ b/cats/src/test/scala/magnolify/cats/TestEq.scala @@ -18,12 +18,14 @@ package magnolify.cats import cats.Eq import magnolify.cats.semiauto.EqDerivation +import magnolify.shared.UnsafeEnum import magnolify.test.ADT._ import magnolify.test.JavaEnums import magnolify.test.Simple._ -import magnolify.shared.UnsafeEnum +import org.joda.{time => joda} import java.net.URI +import java.nio.ByteBuffer import java.time._ object TestEq { @@ -38,13 +40,22 @@ object TestEq { xs.size == ys.size && (xs zip ys).forall((eq.eqv _).tupled) } - // time - implicit lazy val eqInstant: Eq[Instant] = Eq.by(_.toEpochMilli) - implicit lazy val eqLocalDate: Eq[LocalDate] = Eq.by(_.toEpochDay) - implicit lazy val eqLocalTime: Eq[LocalTime] = Eq.by(_.toNanoOfDay) - implicit lazy val eqLocalDateTime: Eq[LocalDateTime] = Eq.by(_.toEpochSecond(ZoneOffset.UTC)) - implicit lazy val eqOffsetTime: Eq[OffsetTime] = Eq.by(_.toLocalTime.toNanoOfDay) - implicit lazy val eqDuration: Eq[Duration] = Eq.by(_.toMillis) + // java + implicit val eqByteBuffer: Eq[ByteBuffer] = Eq.by(_.array()) + + // java-time + implicit lazy val eqInstant: Eq[Instant] = Eq.fromUniversalEquals + implicit lazy val eqLocalDate: Eq[LocalDate] = Eq.fromUniversalEquals + implicit lazy val eqLocalTime: Eq[LocalTime] = Eq.fromUniversalEquals + implicit lazy val eqLocalDateTime: Eq[LocalDateTime] = Eq.fromUniversalEquals + implicit lazy val eqOffsetTime: Eq[OffsetTime] = Eq.fromUniversalEquals + implicit lazy val eqDuration: Eq[Duration] = Eq.fromUniversalEquals + + // joda-time + implicit val eqJodaDate: Eq[joda.LocalDate] = Eq.fromUniversalEquals + implicit val eqJodaDateTime: Eq[joda.DateTime] = Eq.fromUniversalEquals + implicit val eqJodaLocalTime: Eq[joda.LocalTime] = Eq.fromUniversalEquals + implicit val eqJodaLocalDateTime: Eq[joda.LocalDateTime] = Eq.fromUniversalEquals // enum implicit lazy val eqJavaEnum: Eq[JavaEnums.Color] = Eq.fromUniversalEquals diff --git a/scalacheck/src/test/scala/magnolify/scalacheck/TestArbitrary.scala b/scalacheck/src/test/scala/magnolify/scalacheck/TestArbitrary.scala index fe0edfd8e..48f21cd0d 100644 --- a/scalacheck/src/test/scala/magnolify/scalacheck/TestArbitrary.scala +++ b/scalacheck/src/test/scala/magnolify/scalacheck/TestArbitrary.scala @@ -21,13 +21,20 @@ import magnolify.shared.UnsafeEnum import magnolify.test.ADT._ import magnolify.test.JavaEnums import magnolify.test.Simple._ +import org.joda.{time => joda} import org.scalacheck._ -import java.time._ import java.net.URI +import java.nio.ByteBuffer +import java.time._ object TestArbitrary { - // time + // java + implicit val arbByteBuffer: Arbitrary[ByteBuffer] = Arbitrary { + Arbitrary.arbitrary[Array[Byte]].map(ByteBuffer.wrap) + } + + // java-time implicit lazy val arbInstant: Arbitrary[Instant] = Arbitrary(Gen.posNum[Long].map(Instant.ofEpochMilli)) implicit lazy val arbLocalDate: Arbitrary[LocalDate] = @@ -41,6 +48,28 @@ object TestArbitrary { implicit lazy val arbDuration: Arbitrary[Duration] = Arbitrary(Gen.posNum[Long].map(Duration.ofMillis)) + // joda-time + implicit val arbJodaDate: Arbitrary[joda.LocalDate] = Arbitrary { + Arbitrary.arbitrary[LocalDate].map { ld => + new joda.LocalDate(ld.getYear, ld.getMonthValue, ld.getDayOfMonth) + } + } + implicit val arbJodaDateTime: Arbitrary[joda.DateTime] = Arbitrary { + Arbitrary.arbitrary[Instant].map { i => + new joda.DateTime(i.toEpochMilli, joda.DateTimeZone.UTC) + } + } + implicit val arbJodaLocalTime: Arbitrary[joda.LocalTime] = Arbitrary { + Arbitrary.arbitrary[LocalTime].map { lt => + joda.LocalTime.fromMillisOfDay(lt.toNanoOfDay / 1000) + } + } + implicit val arbJodaLocalDateTime: Arbitrary[joda.LocalDateTime] = Arbitrary { + Arbitrary.arbitrary[LocalDateTime].map { ldt => + joda.LocalDateTime.parse(ldt.toString) + } + } + // enum implicit lazy val arbJavaEnum: Arbitrary[JavaEnums.Color] = Arbitrary( Gen.oneOf(JavaEnums.Color.values.toSeq)