From 003ff06d311664bcf32a1412bcc3e42521674864 Mon Sep 17 00:00:00 2001 From: chr12c Date: Fri, 8 Sep 2023 18:44:19 +0100 Subject: [PATCH] non empty map codec support --- .../core/src/main/scala/vulcan/Codec.scala | 20 +++++- .../src/test/scala/vulcan/CodecSpec.scala | 71 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/vulcan/Codec.scala b/modules/core/src/main/scala/vulcan/Codec.scala index 453c7ec7..faade74e 100644 --- a/modules/core/src/main/scala/vulcan/Codec.scala +++ b/modules/core/src/main/scala/vulcan/Codec.scala @@ -7,7 +7,7 @@ package vulcan import cats.{Invariant, Show, ~>} -import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} +import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptyMap, NonEmptySet, NonEmptyVector} import cats.free.FreeApplicative import cats.implicits._ @@ -23,7 +23,7 @@ import vulcan.Avro.Bytes import vulcan.internal.{Deserializer, Serializer} import scala.annotation.implicitNotFound -import scala.collection.immutable.SortedSet +import scala.collection.immutable.{SortedMap, SortedSet} import vulcan.internal.converters.collection._ import vulcan.internal.syntax._ import vulcan.internal.schema.adaptForSchema @@ -972,6 +972,22 @@ object Codec extends CodecCompanionCompat { )(_.toVector) .withTypeName("NonEmptyVector") + /** + * @group Cats + */ + implicit final def nonEmptyMap[A]( + implicit codec: Codec[A] + ): Codec.Aux[Avro.Map[codec.AvroType], NonEmptyMap[String, A]] = + Codec + .map[A] + .imapError( + map => + NonEmptyMap + .fromMap(SortedMap.from(map)) + .toRight(AvroError.decodeEmptyCollection) + )(_.toSortedMap) + .withTypeName("NonEmptyMap") + /** * @group General */ diff --git a/modules/core/src/test/scala/vulcan/CodecSpec.scala b/modules/core/src/test/scala/vulcan/CodecSpec.scala index 7fb04643..676ed4e1 100644 --- a/modules/core/src/test/scala/vulcan/CodecSpec.scala +++ b/modules/core/src/test/scala/vulcan/CodecSpec.scala @@ -1556,6 +1556,77 @@ final class CodecSpec extends BaseSpec with CodecSpecHelpers { } } + describe("nonEmptyMap") { + describe("schema") { + it("should be encoded as map") { + assertSchemaIs[NonEmptyMap[String, Int]] { + """{"type":"map","values":"int"}""" + } + } + } + + describe("encode") { + it("should encode as java map using encoder for value") { + assertEncodeIs[NonEmptyMap[String, Int]]( + NonEmptyMap.of("key1" -> 1, "key2" -> 2, "key3" -> 3), + Right( + Map(Avro.String("key1") -> 1, Avro.String("key2") -> 2, Avro.String("key3") -> 3).asJava + ) + ) + } + } + + describe("decode") { + it("should error if schema is not map") { + assertDecodeError[NonEmptyMap[String, Int]]( + unsafeEncode[NonEmptyMap[String, Int]](NonEmptyMap.one("key", 1)), + SchemaBuilder.builder().intType(), + "Error decoding NonEmptyMap: Error decoding Map: Got unexpected schema type INT, expected schema type MAP" + ) + } + + it("should error if value is not java.util.Map") { + assertDecodeError[NonEmptyMap[String, Int]]( + 123, + unsafeSchema[NonEmptyMap[String, Int]], + "Error decoding NonEmptyMap: Error decoding Map: Got unexpected type java.lang.Integer, expected type java.util.Map" + ) + } + + it("should error if keys are not strings") { + assertDecodeError[NonEmptyMap[String, Int]]( + NonEmptyMap.one(1, 2).toSortedMap.asJava, + unsafeSchema[NonEmptyMap[String, Int]], + "Error decoding NonEmptyMap: Error decoding Map: Got unexpected map key with type java.lang.Integer, expected Utf8" + ) + } + + it("should error if any keys are null") { + assertDecodeError[NonEmptyMap[String, Int]]( + Map((null, 2)).asJava, + unsafeSchema[NonEmptyMap[String, Int]], + "Error decoding NonEmptyMap: Error decoding Map: Got unexpected map key with type null, expected Utf8" + ) + } + + it("should error on empty collection") { + assertDecodeError[NonEmptyMap[String, Int]]( + unsafeEncode(Map.empty[String, Int]), + unsafeSchema[NonEmptyMap[String, Int]], + "Error decoding NonEmptyMap: Got unexpected empty collection" + ) + } + + it("should decode to map using decoder for value") { + val value = NonEmptyMap.of("key1" -> 1, "key2" -> 2, "key3" -> 3) + assertDecodeIs[NonEmptyMap[String, Int]]( + unsafeEncode[NonEmptyMap[String, Int]](value), + Right(value) + ) + } + } + } + describe("option") { describe("schema") { it("should be encoded as union") {