From c9398ac031902e8c221ed621c297ee40c5f49d90 Mon Sep 17 00:00:00 2001 From: Petter Kamfjord Date: Wed, 14 Aug 2024 09:47:33 +0200 Subject: [PATCH 1/3] JsonFieldEncoder for uuid --- .../scala/zio/json/JsonFieldDecoder.scala | 22 ++++++++++++++++ .../scala/zio/json/JsonFieldEncoder.scala | 7 ++++++ .../src/test/scala/zio/json/DecoderSpec.scala | 25 +++++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 3 +++ 4 files changed, 57 insertions(+) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index 68fc213cf..a9b1eac70 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -15,6 +15,10 @@ */ package zio.json +import zio.json.uuid.UUIDParser + +import java.util.UUID + /** When decoding a JSON Object, we only allow the keys that implement this interface. */ trait JsonFieldDecoder[+A] { self => @@ -64,4 +68,22 @@ object JsonFieldDecoder { case n: NumberFormatException => Left(s"Invalid Long: '$str': $n") } } + + implicit val uuid: JsonFieldDecoder[java.util.UUID] = mapStringOrFail { str => + try { + Right(UUIDParser.unsafeParse(str)) + } catch { + case iae: IllegalArgumentException => Left(s"Invalid UUID: ${iae.getMessage}") + } + } + + // use this instead of `string.mapOrFail` in supertypes (to prevent class initialization error at runtime) + private[json] def mapStringOrFail[A](f: String => Either[String, A]): JsonFieldDecoder[A] = + new JsonFieldDecoder[A] { + def unsafeDecodeField(trace: List[JsonError], in: String): A = + f(string.unsafeDecodeField(trace, in)) match { + case Left(err) => throw JsonDecoder.UnsafeJson(JsonError.Message(err) :: trace) + case Right(value) => value + } + } } diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala index 706bac5ae..963b80dcc 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala @@ -15,6 +15,9 @@ */ package zio.json +import zio.json.ast.Json +import zio.json.internal.Write + /** When encoding a JSON Object, we only allow keys that implement this interface. */ trait JsonFieldEncoder[-A] { self => @@ -38,4 +41,8 @@ object JsonFieldEncoder { implicit val long: JsonFieldEncoder[Long] = JsonFieldEncoder[String].contramap(_.toString) + + implicit val uuid: JsonFieldEncoder[java.util.UUID] = + JsonFieldEncoder[String].contramap(_.toString) + } diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index acb23e86f..75a9ee812 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -241,6 +241,31 @@ object DecoderSpec extends ZIOSpecDefault { val jsonStr = JsonEncoder[Map[String, String]].encodeJson(expected, None) assert(jsonStr.fromJson[Map[String, String]])(isRight(equalTo(expected))) }, + test("Map with UUID keys") { + def expectedMap(str: String): Map[UUID, String] = Map(UUID.fromString(str) -> "value") + + val ok1 = """{"64d7c38d-2afd-4514-9832-4e70afe4b0f8": "value"}""" + val ok2 = """{"0000000064D7C38D-FD-14-32-70AFE4B0f8": "value"}""" + val ok3 = """{"0-0-0-0-0": "value"}""" + val bad1 = """{"": "value"}""" + val bad2 = """{"64d7c38d-2afd-4514-9832-4e70afe4b0f80": "value"}""" + val bad3 = """{"64d7c38d-2afd-4514-983-4e70afe4b0f80": "value"}""" + val bad4 = """{"64d7c38d-2afd--9832-4e70afe4b0f8": "value"}""" + val bad5 = """{"64d7c38d-2afd-XXXX-9832-4e70afe4b0f8": "value"}""" + val bad6 = """{"64d7c38d-2afd-X-9832-4e70afe4b0f8": "value"}""" + val bad7 = """{"0-0-0-0-00000000000000000": "value"}""" + + assert(ok1.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && + assert(ok2.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && + assert(ok3.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000")))) && + assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && + assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad3.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && + assert(bad4.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && + assert(bad5.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && + assert(bad6.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && + assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" val expected = Chunk("5XL", "2XL", "XL") diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index f8b895ec1..6c0364da0 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -261,6 +261,9 @@ object EncoderSpec extends ZIOSpecDefault { test("Map, custom keys") { assert(Map(1 -> "a").toJson)(equalTo("""{"1":"a"}""")) }, + test("Map, UUID keys") { + assert(Map(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd") -> "abcd").toJson)(equalTo("""{"e142f1aa-6e9e-4352-adfe-7e6eb9814ccd":"abcd"}""")) + }, test("java.util.UUID") { assert(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd").toJson)( equalTo(""""e142f1aa-6e9e-4352-adfe-7e6eb9814ccd"""") From dd4a25a3c999be5fab27ef7cb0cef84e59884cf9 Mon Sep 17 00:00:00 2001 From: Petter Kamfjord Date: Wed, 14 Aug 2024 09:49:26 +0200 Subject: [PATCH 2/3] unused --- zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala | 2 -- zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala | 3 --- 2 files changed, 5 deletions(-) diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala index a9b1eac70..d25f6adf7 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldDecoder.scala @@ -17,8 +17,6 @@ package zio.json import zio.json.uuid.UUIDParser -import java.util.UUID - /** When decoding a JSON Object, we only allow the keys that implement this interface. */ trait JsonFieldDecoder[+A] { self => diff --git a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala index 963b80dcc..d2b1e156c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala @@ -15,9 +15,6 @@ */ package zio.json -import zio.json.ast.Json -import zio.json.internal.Write - /** When encoding a JSON Object, we only allow keys that implement this interface. */ trait JsonFieldEncoder[-A] { self => From c4036599d8812feefd2e82f93dcbafc01d80bc38 Mon Sep 17 00:00:00 2001 From: Petter Kamfjord Date: Wed, 14 Aug 2024 09:50:10 +0200 Subject: [PATCH 3/3] formatting --- .../src/test/scala/zio/json/DecoderSpec.scala | 34 +++++++++++++------ .../src/test/scala/zio/json/EncoderSpec.scala | 4 ++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala index 75a9ee812..73353af82 100644 --- a/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/DecoderSpec.scala @@ -255,16 +255,30 @@ object DecoderSpec extends ZIOSpecDefault { val bad6 = """{"64d7c38d-2afd-X-9832-4e70afe4b0f8": "value"}""" val bad7 = """{"0-0-0-0-00000000000000000": "value"}""" - assert(ok1.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("64d7c38d-2afd-4514-9832-4e70afe4b0f8")))) && - assert(ok2.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("64D7C38D-00FD-0014-0032-0070AfE4B0f8")))) && - assert(ok3.fromJson[Map[UUID, String]])(isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000")))) && - assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && - assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: UUID string too large"))) && - assert(bad3.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80"))) && - assert(bad4.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8"))) && - assert(bad5.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8"))) && - assert(bad6.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8"))) && - assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) + assert(ok1.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("64d7c38d-2afd-4514-9832-4e70afe4b0f8"))) + ) && + assert(ok2.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("64D7C38D-00FD-0014-0032-0070AfE4B0f8"))) + ) && + assert(ok3.fromJson[Map[UUID, String]])( + isRight(equalTo(expectedMap("00000000-0000-0000-0000-000000000000"))) + ) && + assert(bad1.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: "))) && + assert(bad2.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: UUID string too large"))) && + assert(bad3.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-4514-983-4e70afe4b0f80")) + ) && + assert(bad4.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd--9832-4e70afe4b0f8")) + ) && + assert(bad5.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-XXXX-9832-4e70afe4b0f8")) + ) && + assert(bad6.fromJson[Map[UUID, String]])( + isLeft(containsString("Invalid UUID: 64d7c38d-2afd-X-9832-4e70afe4b0f8")) + ) && + assert(bad7.fromJson[Map[UUID, String]])(isLeft(containsString("Invalid UUID: 0-0-0-0-00000000000000000"))) }, test("zio.Chunk") { val jsonStr = """["5XL","2XL","XL"]""" diff --git a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala index 6c0364da0..59f484d48 100644 --- a/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala +++ b/zio-json/shared/src/test/scala/zio/json/EncoderSpec.scala @@ -262,7 +262,9 @@ object EncoderSpec extends ZIOSpecDefault { assert(Map(1 -> "a").toJson)(equalTo("""{"1":"a"}""")) }, test("Map, UUID keys") { - assert(Map(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd") -> "abcd").toJson)(equalTo("""{"e142f1aa-6e9e-4352-adfe-7e6eb9814ccd":"abcd"}""")) + assert(Map(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd") -> "abcd").toJson)( + equalTo("""{"e142f1aa-6e9e-4352-adfe-7e6eb9814ccd":"abcd"}""") + ) }, test("java.util.UUID") { assert(UUID.fromString("e142f1aa-6e9e-4352-adfe-7e6eb9814ccd").toJson)(