From 6395b8ecaef16d3a969d6274877dc30f1b618e2a Mon Sep 17 00:00:00 2001 From: Petter Date: Tue, 20 Aug 2024 08:19:53 +0200 Subject: [PATCH] JsonFieldEncoder for uuid (#1144) * JsonFieldEncoder for uuid * unused * formatting --------- Co-authored-by: Petter Kamfjord --- .../scala/zio/json/JsonFieldDecoder.scala | 20 ++++++++++ .../scala/zio/json/JsonFieldEncoder.scala | 4 ++ .../src/test/scala/zio/json/DecoderSpec.scala | 39 +++++++++++++++++++ .../src/test/scala/zio/json/EncoderSpec.scala | 5 +++ 4 files changed, 68 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..d25f6adf7 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,8 @@ */ package zio.json +import zio.json.uuid.UUIDParser + /** When decoding a JSON Object, we only allow the keys that implement this interface. */ trait JsonFieldDecoder[+A] { self => @@ -64,4 +66,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..d2b1e156c 100644 --- a/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala +++ b/zio-json/shared/src/main/scala/zio/json/JsonFieldEncoder.scala @@ -38,4 +38,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..73353af82 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,45 @@ 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..59f484d48 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,11 @@ 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"""")