From 1b96e024f851e8d8cd3ba5a014a3c9eedcd6c837 Mon Sep 17 00:00:00 2001 From: Keir Lawson Date: Wed, 28 Apr 2021 12:30:21 +0100 Subject: [PATCH 1/3] Add codecs for LocalTime --- .../core/src/main/scala/vulcan/Codec.scala | 45 ++++++- .../src/test/scala/vulcan/CodecSpec.scala | 114 +++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/vulcan/Codec.scala b/modules/core/src/main/scala/vulcan/Codec.scala index 006992fd..b1a7e942 100644 --- a/modules/core/src/main/scala/vulcan/Codec.scala +++ b/modules/core/src/main/scala/vulcan/Codec.scala @@ -14,7 +14,8 @@ import cats.implicits._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -import java.time.{Instant, LocalDate} +import java.time.{Instant, LocalDate, LocalTime} +import java.util.concurrent.TimeUnit import java.util.UUID import org.apache.avro.{Conversions, LogicalTypes, Schema, SchemaBuilder} import org.apache.avro.generic._ @@ -710,6 +711,48 @@ object Codec extends CodecCompanionCompat { } ) + /** + * @group JavaTime + */ + final val localTimeMillis: Codec.Aux[java.lang.Integer, LocalTime] = + Codec.instanceForTypes( + "Integer", + "LocalTime", + Right(LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType())), + { localTime => + val millis = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay()) + Right(java.lang.Integer.valueOf(millis.toInt)) + }, { + case (int: java.lang.Integer, schema) => + val logicalType = schema.getLogicalType() + if (logicalType == LogicalTypes.timeMillis()) { + val nanos = TimeUnit.MILLISECONDS.toNanos(int.toLong) + Right(LocalTime.ofNanoOfDay(nanos)) + } else Left(AvroError.decodeUnexpectedLogicalType(logicalType)) + } + ) + + /** + * @group JavaTime + */ + final val localTimeMicros: Codec.Aux[java.lang.Long, LocalTime] = + Codec.instanceForTypes( + "Long", + "LocalTime", + Right(LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType())), + { localTime => + val micros = TimeUnit.NANOSECONDS.toMicros(localTime.toNanoOfDay()) + Right(java.lang.Long.valueOf(micros)) + }, { + case (long: java.lang.Long, schema) => + val logicalType = schema.getLogicalType() + if (logicalType == LogicalTypes.timeMicros()) { + val nanos = TimeUnit.MICROSECONDS.toNanos(long) + Right(LocalTime.ofNanoOfDay(nanos)) + } else Left(AvroError.decodeUnexpectedLogicalType(logicalType)) + } + ) + /** * @group General */ diff --git a/modules/core/src/test/scala/vulcan/CodecSpec.scala b/modules/core/src/test/scala/vulcan/CodecSpec.scala index 79f52cd6..7eeb2c33 100644 --- a/modules/core/src/test/scala/vulcan/CodecSpec.scala +++ b/modules/core/src/test/scala/vulcan/CodecSpec.scala @@ -4,7 +4,8 @@ import cats.data._ import cats.implicits._ import java.nio.ByteBuffer import java.nio.charset.StandardCharsets -import java.time.{Instant, LocalDate} +import java.time.{Instant, LocalDate, LocalTime} +import java.util.concurrent.TimeUnit import java.util.UUID import org.apache.avro.{Conversions, LogicalTypes, Schema, SchemaBuilder} @@ -1121,6 +1122,117 @@ final class CodecSpec extends BaseSpec with CodecSpecHelpers { } } + describe("localTimeMillis") { + implicit val codec: Codec[LocalTime] = Codec.localTimeMillis + describe("schema") { + it("should be encoded as int with logical type time-millis") { + assertSchemaIs[LocalTime] { + """{"type":"int","logicalType":"time-millis"}""" + } + } + } + + describe("encode") { + it("should encode as int") { + val value = LocalTime.now() + assertEncodeIs[LocalTime]( + value, + Right(java.lang.Integer.valueOf(TimeUnit.NANOSECONDS.toMillis(value.toNanoOfDay()).toInt)) + ) + } + } + + describe("decode") { + it("should error if schema is not int") { + assertDecodeError[LocalTime]( + unsafeEncode(LocalTime.now()), + unsafeSchema[Long], + "Error decoding LocalTime: Got unexpected schema type LONG, expected schema type INT" + ) + } + + it("should error if logical type is not time-millis") { + assertDecodeError[LocalTime]( + unsafeEncode(LocalTime.now()), + unsafeSchema[Int], + "Error decoding LocalTime: Got unexpected missing logical type" + ) + } + + it("should error if value is not int") { + assertDecodeError[LocalTime]( + unsafeEncode(123L), + unsafeSchema[LocalTime], + "Error decoding LocalTime: Got unexpected type java.lang.Long, expected type Integer" + ) + } + + it("should decode int as local time-millis") { + val value = LocalTime.now() + assertDecodeIs[LocalTime]( + unsafeEncode(value), + Right(LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(TimeUnit.NANOSECONDS.toMillis(value.toNanoOfDay())))) + ) + } + } + } + + describe("localTimeMicros") { + implicit val codec: Codec[LocalTime] = Codec.localTimeMicros + describe("schema") { + it("should be encoded as int with logical type time-micros") { + assertSchemaIs[LocalTime] { + """{"type":"long","logicalType":"time-micros"}""" + } + } + } + + describe("encode") { + it("should encode as long") { + val value = LocalTime.now() + assertEncodeIs[LocalTime]( + value, + Right(java.lang.Long.valueOf(TimeUnit.NANOSECONDS.toMicros(value.toNanoOfDay()))) + ) + } + } + + describe("decode") { + it("should error if schema is not int") { + assertDecodeError[LocalTime]( + unsafeEncode(LocalTime.now()), + unsafeSchema[Int], + "Error decoding LocalTime: Got unexpected schema type INT, expected schema type LONG" + ) + } + + it("should error if logical type is not time-micros") { + assertDecodeError[LocalTime]( + unsafeEncode(LocalTime.now()), + unsafeSchema[Long], + "Error decoding LocalTime: Got unexpected missing logical type" + ) + } + + it("should error if value is not long") { + assertDecodeError[LocalTime]( + unsafeEncode(123), + unsafeSchema[LocalTime], + "Error decoding LocalTime: Got unexpected type java.lang.Integer, expected type Long" + ) + } + + it("should decode int as local time-micros") { + val value = LocalTime.now() + assertDecodeIs[LocalTime]( + unsafeEncode(value), + Right(LocalTime.ofNanoOfDay(TimeUnit.MICROSECONDS.toNanos(TimeUnit.NANOSECONDS.toMicros(value.toNanoOfDay())))) + ) + } + } + } + + describe("long") { describe("schema") { it("should be encoded as long") { From 74d286a9881efecc2eb9bf92b13a80771770c1c2 Mon Sep 17 00:00:00 2001 From: Keir Lawson Date: Wed, 28 Apr 2021 13:37:40 +0100 Subject: [PATCH 2/3] Format --- .../core/src/main/scala/vulcan/Codec.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/core/src/main/scala/vulcan/Codec.scala b/modules/core/src/main/scala/vulcan/Codec.scala index b1a7e942..43418e03 100644 --- a/modules/core/src/main/scala/vulcan/Codec.scala +++ b/modules/core/src/main/scala/vulcan/Codec.scala @@ -714,14 +714,14 @@ object Codec extends CodecCompanionCompat { /** * @group JavaTime */ - final val localTimeMillis: Codec.Aux[java.lang.Integer, LocalTime] = + final val localTimeMillis: Codec.Aux[java.lang.Integer, LocalTime] = Codec.instanceForTypes( "Integer", "LocalTime", - Right(LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType())), - { localTime => - val millis = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay()) - Right(java.lang.Integer.valueOf(millis.toInt)) + Right(LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType())), { + localTime => + val millis = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay()) + Right(java.lang.Integer.valueOf(millis.toInt)) }, { case (int: java.lang.Integer, schema) => val logicalType = schema.getLogicalType() @@ -735,14 +735,14 @@ object Codec extends CodecCompanionCompat { /** * @group JavaTime */ - final val localTimeMicros: Codec.Aux[java.lang.Long, LocalTime] = + final val localTimeMicros: Codec.Aux[java.lang.Long, LocalTime] = Codec.instanceForTypes( "Long", "LocalTime", - Right(LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType())), - { localTime => - val micros = TimeUnit.NANOSECONDS.toMicros(localTime.toNanoOfDay()) - Right(java.lang.Long.valueOf(micros)) + Right(LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType())), { + localTime => + val micros = TimeUnit.NANOSECONDS.toMicros(localTime.toNanoOfDay()) + Right(java.lang.Long.valueOf(micros)) }, { case (long: java.lang.Long, schema) => val logicalType = schema.getLogicalType() From fd510eb24dacd597b6ec94e1901eb5cc6dee6790 Mon Sep 17 00:00:00 2001 From: Keir Lawson Date: Wed, 28 Apr 2021 14:54:01 +0100 Subject: [PATCH 3/3] Use truncatedTo to make assertion intent clearer --- modules/core/src/test/scala/vulcan/CodecSpec.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/core/src/test/scala/vulcan/CodecSpec.scala b/modules/core/src/test/scala/vulcan/CodecSpec.scala index 7eeb2c33..57c7cc53 100644 --- a/modules/core/src/test/scala/vulcan/CodecSpec.scala +++ b/modules/core/src/test/scala/vulcan/CodecSpec.scala @@ -6,6 +6,7 @@ import java.nio.ByteBuffer import java.nio.charset.StandardCharsets import java.time.{Instant, LocalDate, LocalTime} import java.util.concurrent.TimeUnit +import java.time.temporal.ChronoUnit import java.util.UUID import org.apache.avro.{Conversions, LogicalTypes, Schema, SchemaBuilder} @@ -1171,7 +1172,7 @@ final class CodecSpec extends BaseSpec with CodecSpecHelpers { val value = LocalTime.now() assertDecodeIs[LocalTime]( unsafeEncode(value), - Right(LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(TimeUnit.NANOSECONDS.toMillis(value.toNanoOfDay())))) + Right(value.truncatedTo(ChronoUnit.MILLIS)) ) } } @@ -1226,7 +1227,7 @@ final class CodecSpec extends BaseSpec with CodecSpecHelpers { val value = LocalTime.now() assertDecodeIs[LocalTime]( unsafeEncode(value), - Right(LocalTime.ofNanoOfDay(TimeUnit.MICROSECONDS.toNanos(TimeUnit.NANOSECONDS.toMicros(value.toNanoOfDay())))) + Right(value.truncatedTo(ChronoUnit.MICROS)) ) } }