From 3665127eb809fb25d67461c8205eea0d3a9c5db7 Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 15:56:10 +0200 Subject: [PATCH 1/6] ISSUE-409 wip --- .gitignore | 1 + .../main/scala/enumeratum/EnumFormats.scala | 16 ++++++++-- .../scala/enumeratum/EnumFormatsSpec.scala | 29 ++++++++++++------- .../src/test/scala/enumeratum/ReadError.scala | 10 +++++++ 4 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala diff --git a/.gitignore b/.gitignore index 0f11578f..51da3ab6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ node_modules # Ignore compiled assets /public/assets +.java-version \ No newline at end of file diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index 4ce4c83a..910dcd2c 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -137,7 +137,13 @@ object EnumFormats { case JsString(s) => extract(s) match { case Some(obj) => JsSuccess(obj) - case None => JsError("error.expected.validenumvalue") + case None => + JsError( + JsonValidationError( + "error.expected.validenumvalue", + s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" + ) + ) } case _ => JsError("error.expected.enumstring") @@ -148,7 +154,13 @@ object EnumFormats { )(extract: String => Option[A]): KeyReads[A] = new KeyReads[A] { def readKey(s: String): JsResult[A] = extract(s) match { case Some(obj) => JsSuccess(obj) - case None => JsError("error.expected.validenumvalue") + case None => + JsError( + JsonValidationError( + "error.expected.validenumvalue", + s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" + ) + ) } } } diff --git a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala index c06e2758..571008fc 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala @@ -11,7 +11,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { descriptor = "normal operation", reads = EnumFormats.reads(Dummy), readSuccessExpectations = Map("A" -> Dummy.A), - readErrors = Map("C" -> Seq("error.expected.validenumvalue")), + readErrors = Map("C" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue"))), writes = EnumFormats.writes(Dummy), writeExpectations = Map(Dummy.A -> "A"), formats = EnumFormats.formats(Dummy) @@ -58,7 +58,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "a" -> Dummy.A ), readErrors = Map( - "A" -> Seq("error.expected.validenumvalue") + "A" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.writesLowercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "a"), @@ -86,7 +86,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "C" -> Dummy.c ), readErrors = Map( - "a" -> Seq("error.expected.validenumvalue") + "a" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.writesUppercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "A"), @@ -117,11 +117,19 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { _ => Seq.empty ) + private def errorArgs(jsResult: JsResult[_]): scala.collection.Seq[String] = + jsResult.fold( + _.collect { case (path, errors) => + errors.map(_.args.mkString(", ")).mkString + }, + _ => Seq.empty + ) + private def testScenario( descriptor: String, reads: Reads[Dummy], readSuccessExpectations: Map[String, Dummy], - readErrors: Map[String, Seq[String]], + readErrors: Map[String, ReadError], writes: Writes[Dummy], writeExpectations: Map[Dummy, String], formats: Format[Dummy] @@ -136,14 +144,14 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { private def testReads( reads: Reads[Dummy], expectedSuccesses: Map[String, Dummy], - expectedErrors: Map[String, Seq[String]] + expectedErrors: Map[String, ReadError] ): Unit = describe("Reads") { - val expectedFails: Map[JsValue, Seq[String]] = { + val expectedFails: Map[JsValue, ReadError] = { val withJsValueKeys = expectedErrors.map { case (k, v) => JsString(k) -> v } // Add standard errors (withJsValueKeys ++ Map( - JsNumber(2) -> Seq("error.expected.enumstring"), - JsString("D") -> Seq("error.expected.validenumvalue") + JsNumber(2) -> ReadError.onlyMessages(Seq("error.expected.enumstring")), + JsString("D") -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) )).toMap } @@ -157,7 +165,8 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { expectedFails.foreach { case (k, v) => val result = reads.reads(k) result.isError shouldBe true - errorMessages(result) shouldBe v + errorMessages(result) shouldBe v.errorMessages + errorArgs(result) shouldBe v.errorArgs } } } @@ -178,7 +187,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { private def testFormats( formats: Format[Dummy], expectedReadSuccesses: Map[String, Dummy], - expectedReadErrors: Map[String, Seq[String]], + expectedReadErrors: Map[String, ReadError], expectedWrites: Map[Dummy, String] ): Unit = describe("Formats") { testReads(formats, expectedReadSuccesses, expectedReadErrors) diff --git a/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala b/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala new file mode 100644 index 00000000..ecdfd437 --- /dev/null +++ b/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala @@ -0,0 +1,10 @@ +package enumeratum + +import scala.collection.immutable + +case class ReadError(errorMessages: Iterable[String], errorArgs: Iterable[String]) + +object ReadError { + def onlyMessages(errorMessages: Iterable[String]): ReadError = + new ReadError(errorMessages, errorArgs = Iterable()) +} From d54deb786f8c0875e72c834fc6ca6ce192c3fa10 Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 16:21:23 +0200 Subject: [PATCH 2/6] ISSUE-409 wip --- .../main/scala/enumeratum/EnumFormats.scala | 8 ++- .../scala/enumeratum/EnumFormatsSpec.scala | 52 +++++++++++++++---- .../src/test/scala/enumeratum/ReadError.scala | 2 - 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index 910dcd2c..a99570b7 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -146,7 +146,13 @@ object EnumFormats { ) } - case _ => JsError("error.expected.enumstring") + case s => + JsError( + JsonValidationError( + "error.expected.enumstring", + s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" + ) + ) } private def readsKeyAndExtracts[A <: EnumEntry]( diff --git a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala index 571008fc..f17ebeee 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala @@ -11,7 +11,12 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { descriptor = "normal operation", reads = EnumFormats.reads(Dummy), readSuccessExpectations = Map("A" -> Dummy.A), - readErrors = Map("C" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue"))), + readErrors = Map( + "C" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: C") + ) + ), writes = EnumFormats.writes(Dummy), writeExpectations = Map(Dummy.A -> "A"), formats = EnumFormats.formats(Dummy) @@ -21,7 +26,12 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { descriptor = "normal operation", reads = EnumFormats.keyReads(Dummy), readSuccessExpectations = Map("A" -> Dummy.A), - readErrors = Map("C" -> Seq("error.expected.validenumvalue")), + readErrors = Map( + "C" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: C") + ) + ), writes = EnumFormats.keyWrites(Dummy), writeExpectations = Map(Dummy.A -> "A") ) @@ -58,7 +68,10 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "a" -> Dummy.A ), readErrors = Map( - "A" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + "A" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") + ) ), writes = EnumFormats.writesLowercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "a"), @@ -72,7 +85,11 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "a" -> Dummy.A ), readErrors = Map( - "A" -> Seq("error.expected.validenumvalue") + "A" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + // FIXME it looks strange + errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") + ) ), writes = EnumFormats.keyWritesLowercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "a") @@ -86,7 +103,10 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "C" -> Dummy.c ), readErrors = Map( - "a" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + "a" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") + ) ), writes = EnumFormats.writesUppercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "A"), @@ -101,7 +121,10 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "C" -> Dummy.c ), readErrors = Map( - "a" -> Seq("error.expected.validenumvalue") + "a" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") + ) ), writes = EnumFormats.keyWritesUppercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "A") @@ -150,8 +173,14 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { val withJsValueKeys = expectedErrors.map { case (k, v) => JsString(k) -> v } // Add standard errors (withJsValueKeys ++ Map( - JsNumber(2) -> ReadError.onlyMessages(Seq("error.expected.enumstring")), - JsString("D") -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + JsNumber(2) -> ReadError( + errorMessages = Seq("error.expected.enumstring"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: 2") + ), + JsString("D") -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: D") + ) )).toMap } @@ -198,7 +227,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { descriptor: String, reads: KeyReads[Dummy], readSuccessExpectations: Map[String, Dummy], - readErrors: Map[String, Seq[String]], + readErrors: Map[String, ReadError], writes: KeyWrites[Dummy], writeExpectations: Map[Dummy, String] ): Unit = describe(descriptor) { @@ -209,7 +238,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { private def testKeyReads( reads: KeyReads[Dummy], expectedSuccesses: Map[String, Dummy], - expectedErrors: Map[String, Seq[String]] + expectedErrors: Map[String, ReadError] ): Unit = describe("KeyReads") { it("should create a KeyReads that works with valid values") { expectedSuccesses.foreach { case (name, expected) => @@ -221,7 +250,8 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { expectedErrors.foreach { case (k, v) => val result = reads.readKey(k) result.isError shouldBe true - errorMessages(result) shouldBe v + errorMessages(result) shouldBe v.errorMessages + errorArgs(result) shouldBe v.errorArgs } } } diff --git a/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala b/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala index ecdfd437..dfbdc74d 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/ReadError.scala @@ -1,7 +1,5 @@ package enumeratum -import scala.collection.immutable - case class ReadError(errorMessages: Iterable[String], errorArgs: Iterable[String]) object ReadError { From bf079f90bbe6958d464af544c4ac5a2ba1b01580 Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 17:14:37 +0200 Subject: [PATCH 3/6] ISSUE-409 wip --- .../main/scala/enumeratum/EnumFormats.scala | 54 +++++++++++-------- .../scala/enumeratum/EnumFormatsSpec.scala | 1 - 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index a99570b7..8fd80280 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -15,37 +15,47 @@ object EnumFormats { */ def reads[A <: EnumEntry]( @deprecatedName(Symbol("enum")) e: Enum[A], - insensitive: Boolean = false + insensitive: Boolean = false, + detailedError: Boolean = false ): Reads[A] = - readsAndExtracts[A](e) { s => + readsAndExtracts[A](e, detailedError) { s => if (insensitive) e.withNameInsensitiveOption(s) else e.withNameOption(s) } - def readsLowercaseOnly[A <: EnumEntry](@deprecatedName(Symbol("enum")) e: Enum[A]): Reads[A] = - readsAndExtracts[A](e)(e.withNameLowercaseOnlyOption) + def readsLowercaseOnly[A <: EnumEntry]( + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false + ): Reads[A] = + readsAndExtracts[A](e, detailedError)(e.withNameLowercaseOnlyOption) - def readsUppercaseOnly[A <: EnumEntry](@deprecatedName(Symbol("enum")) e: Enum[A]): Reads[A] = - readsAndExtracts[A](e)(e.withNameUppercaseOnlyOption) + def readsUppercaseOnly[A <: EnumEntry]( + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false + ): Reads[A] = + readsAndExtracts[A](e, detailedError)(e.withNameUppercaseOnlyOption) def keyReads[A <: EnumEntry]( @deprecatedName(Symbol("enum")) e: Enum[A], - insensitive: Boolean = false + insensitive: Boolean = false, + detailedError: Boolean = false ): KeyReads[A] = - readsKeyAndExtracts[A](e) { s => + readsKeyAndExtracts[A](e, detailedError) { s => if (insensitive) e.withNameInsensitiveOption(s) else e.withNameOption(s) } def keyReadsLowercaseOnly[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false ): KeyReads[A] = - readsKeyAndExtracts[A](e)(e.withNameLowercaseOnlyOption) + readsKeyAndExtracts[A](e, detailedError)(e.withNameLowercaseOnlyOption) def keyReadsUppercaseOnly[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false ): KeyReads[A] = - readsKeyAndExtracts[A](e)(e.withNameUppercaseOnlyOption) + readsKeyAndExtracts[A](e, detailedError)(e.withNameUppercaseOnlyOption) /** Returns a Json writes for a given enum [[Enum]] */ @@ -132,41 +142,39 @@ object EnumFormats { // --- private def readsAndExtracts[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false )(extract: String => Option[A]): Reads[A] = Reads[A] { case JsString(s) => extract(s) match { case Some(obj) => JsSuccess(obj) - case None => + case None if detailedError => JsError( JsonValidationError( "error.expected.validenumvalue", s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" ) ) + case None => JsError("error.expected.validenumvalue") } - case s => - JsError( - JsonValidationError( - "error.expected.enumstring", - s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" - ) - ) + case _ => JsError("error.expected.enumstring") } private def readsKeyAndExtracts[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false )(extract: String => Option[A]): KeyReads[A] = new KeyReads[A] { def readKey(s: String): JsResult[A] = extract(s) match { case Some(obj) => JsSuccess(obj) - case None => + case None if detailedError => JsError( JsonValidationError( "error.expected.validenumvalue", s"valid enum values are: (${e.values.map(_.entryName).mkString(", ")}), but provided: $s" ) ) + case None => JsError("error.expected.validenumvalue") } } } diff --git a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala index f17ebeee..cb14d7c7 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala @@ -87,7 +87,6 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { readErrors = Map( "A" -> ReadError( errorMessages = Seq("error.expected.validenumvalue"), - // FIXME it looks strange errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") ) ), From ed57ea2139547224636d6ec1f4fa9f1494394cd9 Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 17:45:11 +0200 Subject: [PATCH 4/6] ISSUE-409 wip --- .../main/scala/enumeratum/EnumFormats.scala | 15 ++- .../PlayDetailedErrorJsonEnum.scala | 20 +++ .../scala/enumeratum/EnumFormatsSpec.scala | 115 ++++++++++++------ 3 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 enumeratum-play-json/src/main/scala/enumeratum/PlayDetailedErrorJsonEnum.scala diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index 8fd80280..5e1cd7da 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -112,9 +112,10 @@ object EnumFormats { */ def formats[A <: EnumEntry]( @deprecatedName(Symbol("enum")) e: Enum[A], - insensitive: Boolean = false + insensitive: Boolean = false, + detailedError: Boolean = false ): Format[A] = { - Format(reads(e, insensitive), writes(e)) + Format(reads(e, insensitive, detailedError), writes(e)) } /** Returns a Json format for a given enum [[Enum]] for handling lower case transformations @@ -123,9 +124,10 @@ object EnumFormats { * The enum */ def formatsLowerCaseOnly[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false ): Format[A] = { - Format(readsLowercaseOnly(e), writesLowercaseOnly(e)) + Format(readsLowercaseOnly(e, detailedError), writesLowercaseOnly(e)) } /** Returns a Json format for a given enum [[Enum]] for handling upper case transformations @@ -134,9 +136,10 @@ object EnumFormats { * The enum */ def formatsUppercaseOnly[A <: EnumEntry]( - @deprecatedName(Symbol("enum")) e: Enum[A] + @deprecatedName(Symbol("enum")) e: Enum[A], + detailedError: Boolean = false ): Format[A] = { - Format(readsUppercaseOnly(e), writesUppercaseOnly(e)) + Format(readsUppercaseOnly(e, detailedError), writesUppercaseOnly(e)) } // --- diff --git a/enumeratum-play-json/src/main/scala/enumeratum/PlayDetailedErrorJsonEnum.scala b/enumeratum-play-json/src/main/scala/enumeratum/PlayDetailedErrorJsonEnum.scala new file mode 100644 index 00000000..a4bf1529 --- /dev/null +++ b/enumeratum-play-json/src/main/scala/enumeratum/PlayDetailedErrorJsonEnum.scala @@ -0,0 +1,20 @@ +package enumeratum + +import play.api.libs.json._ + +trait PlayDetailedErrorJsonEnum[A <: EnumEntry] { self: Enum[A] => + implicit val keyWrites: KeyWrites[A] = EnumFormats.keyWrites(this) + + implicit def contraKeyWrites[K <: A]: KeyWrites[K] = { + val w = this.keyWrites + + new KeyWrites[K] { + def writeKey(k: K) = w.writeKey(k) + } + } + + implicit val keyReads: KeyReads[A] = EnumFormats.keyReads(this, detailedError = true) + + implicit val jsonFormat: Format[A] = EnumFormats.formats(this, detailedError = true) + implicit def contraJsonWrites[B <: A]: Writes[B] = jsonFormat.contramap[B](b => b: A) +} diff --git a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala index cb14d7c7..7d11044a 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala @@ -11,6 +11,18 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { descriptor = "normal operation", reads = EnumFormats.reads(Dummy), readSuccessExpectations = Map("A" -> Dummy.A), + readErrors = Map( + "C" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + ), + writes = EnumFormats.writes(Dummy), + writeExpectations = Map(Dummy.A -> "A"), + formats = EnumFormats.formats(Dummy) + ) + + testDetailedErrorScenario( + descriptor = "normal operation with detailed error", + reads = EnumFormats.reads(Dummy, detailedError = true), + readSuccessExpectations = Map("A" -> Dummy.A), readErrors = Map( "C" -> ReadError( errorMessages = Seq("error.expected.validenumvalue"), @@ -19,13 +31,24 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { ), writes = EnumFormats.writes(Dummy), writeExpectations = Map(Dummy.A -> "A"), - formats = EnumFormats.formats(Dummy) + formats = EnumFormats.formats(Dummy, detailedError = true) ) testKeyScenario( descriptor = "normal operation", reads = EnumFormats.keyReads(Dummy), readSuccessExpectations = Map("A" -> Dummy.A), + readErrors = Map( + "C" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + ), + writes = EnumFormats.keyWrites(Dummy), + writeExpectations = Map(Dummy.A -> "A") + ) + + testKeyScenario( + descriptor = "normal operation with detailed error", + reads = EnumFormats.keyReads(Dummy, detailedError = true), + readSuccessExpectations = Map("A" -> Dummy.A), readErrors = Map( "C" -> ReadError( errorMessages = Seq("error.expected.validenumvalue"), @@ -68,10 +91,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "a" -> Dummy.A ), readErrors = Map( - "A" -> ReadError( - errorMessages = Seq("error.expected.validenumvalue"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") - ) + "A" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.writesLowercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "a"), @@ -85,10 +105,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "a" -> Dummy.A ), readErrors = Map( - "A" -> ReadError( - errorMessages = Seq("error.expected.validenumvalue"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") - ) + "A" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.keyWritesLowercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "a") @@ -102,10 +119,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "C" -> Dummy.c ), readErrors = Map( - "a" -> ReadError( - errorMessages = Seq("error.expected.validenumvalue"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") - ) + "a" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.writesUppercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "A"), @@ -120,10 +134,7 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { "C" -> Dummy.c ), readErrors = Map( - "a" -> ReadError( - errorMessages = Seq("error.expected.validenumvalue"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") - ) + "a" -> ReadError.onlyMessages(Seq("error.expected.validenumvalue")) ), writes = EnumFormats.keyWritesUppercaseOnly(Dummy), writeExpectations = Map(Dummy.A -> "A") @@ -133,21 +144,39 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { private def errorMessages(jsResult: JsResult[_]): scala.collection.Seq[String] = jsResult.fold( - _.collect { case (path, errors) => + _.collect { case (_, errors) => errors.map(_.message).mkString }, _ => Seq.empty ) private def errorArgs(jsResult: JsResult[_]): scala.collection.Seq[String] = - jsResult.fold( - _.collect { case (path, errors) => - errors.map(_.args.mkString(", ")).mkString - }, - _ => Seq.empty - ) + jsResult + .fold( + _.collect { case (_, errors) => + errors.flatMap(_.args) + }, + _ => Seq.empty + ) + .flatten + .map(_.toString) private def testScenario( + descriptor: String, + reads: Reads[Dummy], + readSuccessExpectations: Map[String, Dummy], + readErrors: Map[String, ReadError], + writes: Writes[Dummy], + writeExpectations: Map[Dummy, String], + formats: Format[Dummy], + detailedError: Boolean = false + ): Unit = describe(descriptor) { + testReads(reads, readSuccessExpectations, readErrors, detailedError) + testWrites(writes, writeExpectations) + testFormats(formats, readSuccessExpectations, readErrors, writeExpectations, detailedError) + } + + private def testDetailedErrorScenario( descriptor: String, reads: Reads[Dummy], readSuccessExpectations: Map[String, Dummy], @@ -156,9 +185,15 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { writeExpectations: Map[Dummy, String], formats: Format[Dummy] ): Unit = describe(descriptor) { - testReads(reads, readSuccessExpectations, readErrors) + testReads(reads, readSuccessExpectations, readErrors, detailedError = true) testWrites(writes, writeExpectations) - testFormats(formats, readSuccessExpectations, readErrors, writeExpectations) + testFormats( + formats, + readSuccessExpectations, + readErrors, + writeExpectations, + detailedError = true + ) } /** Shared scenarios for testing Reads @@ -166,20 +201,22 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { private def testReads( reads: Reads[Dummy], expectedSuccesses: Map[String, Dummy], - expectedErrors: Map[String, ReadError] + expectedErrors: Map[String, ReadError], + detailedError: Boolean ): Unit = describe("Reads") { val expectedFails: Map[JsValue, ReadError] = { val withJsValueKeys = expectedErrors.map { case (k, v) => JsString(k) -> v } // Add standard errors (withJsValueKeys ++ Map( - JsNumber(2) -> ReadError( - errorMessages = Seq("error.expected.enumstring"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: 2") - ), - JsString("D") -> ReadError( - errorMessages = Seq("error.expected.validenumvalue"), - errorArgs = Seq("valid enum values are: (A, B, c), but provided: D") - ) + JsNumber(2) -> ReadError.onlyMessages(Seq("error.expected.enumstring")), + JsString("D") -> (if (detailedError) { + ReadError( + Seq("error.expected.validenumvalue"), + Seq("valid enum values are: (A, B, c), but provided: D") + ) + } else { + ReadError.onlyMessages(Seq("error.expected.validenumvalue")) + }) )).toMap } @@ -194,7 +231,8 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { val result = reads.reads(k) result.isError shouldBe true errorMessages(result) shouldBe v.errorMessages - errorArgs(result) shouldBe v.errorArgs + val strings = errorArgs(result) + strings shouldBe v.errorArgs } } } @@ -216,9 +254,10 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { formats: Format[Dummy], expectedReadSuccesses: Map[String, Dummy], expectedReadErrors: Map[String, ReadError], - expectedWrites: Map[Dummy, String] + expectedWrites: Map[Dummy, String], + detailedError: Boolean ): Unit = describe("Formats") { - testReads(formats, expectedReadSuccesses, expectedReadErrors) + testReads(formats, expectedReadSuccesses, expectedReadErrors, detailedError) testWrites(formats, expectedWrites) } From 0d950d2225dc9597811107fdff57d0f531e9fdeb Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 18:42:16 +0200 Subject: [PATCH 5/6] ISSUE-409 wip --- .../main/scala/enumeratum/EnumFormats.scala | 4 +- .../src/test/scala/enumeratum/Dummy.scala | 8 ++ .../scala/enumeratum/EnumFormatsSpec.scala | 93 +++++++++++++++++++ .../scala/enumeratum/PlayJsonEnumSpec.scala | 28 +++++- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala index 5e1cd7da..41677651 100644 --- a/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala +++ b/enumeratum-play-json/src/main/scala/enumeratum/EnumFormats.scala @@ -146,7 +146,7 @@ object EnumFormats { private def readsAndExtracts[A <: EnumEntry]( @deprecatedName(Symbol("enum")) e: Enum[A], - detailedError: Boolean = false + detailedError: Boolean )(extract: String => Option[A]): Reads[A] = Reads[A] { case JsString(s) => extract(s) match { @@ -166,7 +166,7 @@ object EnumFormats { private def readsKeyAndExtracts[A <: EnumEntry]( @deprecatedName(Symbol("enum")) e: Enum[A], - detailedError: Boolean = false + detailedError: Boolean )(extract: String => Option[A]): KeyReads[A] = new KeyReads[A] { def readKey(s: String): JsResult[A] = extract(s) match { case Some(obj) => JsSuccess(obj) diff --git a/enumeratum-play-json/src/test/scala/enumeratum/Dummy.scala b/enumeratum-play-json/src/test/scala/enumeratum/Dummy.scala index 1a5c5c8f..a8a36382 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/Dummy.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/Dummy.scala @@ -37,3 +37,11 @@ object UppercaseDummy extends Enum[UppercaseDummy] with PlayUppercaseJsonEnum[Up case object Cherry extends UppercaseDummy val values = findValues } + +sealed trait Operation extends EnumEntry +object Operation extends Enum[Operation] with PlayDetailedErrorJsonEnum[Operation] { + val values = findValues + case object Eq extends Operation + case object Add extends Operation + case object Not extends Operation +} diff --git a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala index 7d11044a..75160b6e 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/EnumFormatsSpec.scala @@ -84,6 +84,31 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { writeExpectations = Map(Dummy.A -> "A") ) + testDetailedErrorScenario( + descriptor = "case insensitive with detailed error", + reads = EnumFormats.reads(e = Dummy, insensitive = true, detailedError = true), + readSuccessExpectations = Map( + "A" -> Dummy.A, + "a" -> Dummy.A + ), + readErrors = Map.empty, + writes = EnumFormats.writes(Dummy), + writeExpectations = Map(Dummy.A -> "A"), + formats = EnumFormats.formats(e = Dummy, insensitive = true, detailedError = true) + ) + + testKeyScenario( + descriptor = "case insensitive with detailed error", + reads = EnumFormats.keyReads(e = Dummy, insensitive = true, detailedError = true), + readSuccessExpectations = Map( + "A" -> Dummy.A, + "a" -> Dummy.A + ), + readErrors = Map.empty, + writes = EnumFormats.keyWrites(Dummy), + writeExpectations = Map(Dummy.A -> "A") + ) + testScenario( descriptor = "lower case transformed", reads = EnumFormats.readsLowercaseOnly(Dummy), @@ -111,6 +136,39 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { writeExpectations = Map(Dummy.A -> "a") ) + testDetailedErrorScenario( + descriptor = "lower case transformed with detailed error", + reads = EnumFormats.readsLowercaseOnly(Dummy, detailedError = true), + readSuccessExpectations = Map( + "a" -> Dummy.A + ), + readErrors = Map( + "A" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") + ) + ), + writes = EnumFormats.writesLowercaseOnly(Dummy), + writeExpectations = Map(Dummy.A -> "a"), + formats = EnumFormats.formatsLowerCaseOnly(Dummy, detailedError = true) + ) + + testKeyScenario( + descriptor = "lower case transformed with detailed error", + reads = EnumFormats.keyReadsLowercaseOnly(Dummy, detailedError = true), + readSuccessExpectations = Map( + "a" -> Dummy.A + ), + readErrors = Map( + "A" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: A") + ) + ), + writes = EnumFormats.keyWritesLowercaseOnly(Dummy), + writeExpectations = Map(Dummy.A -> "a") + ) + testScenario( descriptor = "upper case transformed", reads = EnumFormats.readsUppercaseOnly(Dummy), @@ -140,6 +198,41 @@ class EnumFormatsSpec extends AnyFunSpec with Matchers { writeExpectations = Map(Dummy.A -> "A") ) + testDetailedErrorScenario( + descriptor = "upper case transformed with detailed error", + reads = EnumFormats.readsUppercaseOnly(Dummy, detailedError = true), + readSuccessExpectations = Map( + "A" -> Dummy.A, + "C" -> Dummy.c + ), + readErrors = Map( + "a" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") + ) + ), + writes = EnumFormats.writesUppercaseOnly(Dummy), + writeExpectations = Map(Dummy.A -> "A"), + formats = EnumFormats.formatsUppercaseOnly(Dummy, detailedError = true) + ) + + testKeyScenario( + descriptor = "upper case transformed with detailed error", + reads = EnumFormats.keyReadsUppercaseOnly(Dummy, detailedError = true), + readSuccessExpectations = Map( + "A" -> Dummy.A, + "C" -> Dummy.c + ), + readErrors = Map( + "a" -> ReadError( + errorMessages = Seq("error.expected.validenumvalue"), + errorArgs = Seq("valid enum values are: (A, B, c), but provided: a") + ) + ), + writes = EnumFormats.keyWritesUppercaseOnly(Dummy), + writeExpectations = Map(Dummy.A -> "A") + ) + // Bunch of shared testing methods private def errorMessages(jsResult: JsResult[_]): scala.collection.Seq[String] = diff --git a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala index e3c5fa9d..a867884c 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala @@ -1,9 +1,17 @@ package enumeratum +import org.scalatest.OptionValues._ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -import play.api.libs.json.{JsNumber, JsString, JsSuccess, Json => PlayJson, Writes} -import org.scalatest.OptionValues._ +import play.api.libs.json.{ + JsError, + JsNumber, + JsString, + JsSuccess, + JsonValidationError, + Writes, + Json => PlayJson +} class PlayJsonEnumSpec extends AnyFunSpec with Matchers { @@ -62,6 +70,22 @@ class PlayJsonEnumSpec extends AnyFunSpec with Matchers { Map(UppercaseDummy.Apple -> 4) ) } + + describe("detailed error") { + it("should work with valid values") { + JsString("Add").asOpt[Operation].value shouldBe Operation.Add + JsString("Eq").asOpt[Operation].value shouldBe Operation.Eq + } + + it("should fail with detailed error message") { + val jsError = JsString("Subtract").validate[Operation] + jsError shouldBe an[JsError] + jsError.asInstanceOf[JsError].errors.head._2.head shouldBe JsonValidationError( + List("error.expected.validenumvalue"), + "valid enum values are: (Eq, Add, Not), but provided: Subtract" + ) + } + } } describe("serialisation") { From d1aedb03c5200d88d47f32e3a78d49bcefeb9343 Mon Sep 17 00:00:00 2001 From: Shalom Sanevich Date: Sun, 24 Nov 2024 20:09:57 +0200 Subject: [PATCH 6/6] ISSUE-409 test in PlayJsonEnumSpec --- .../scala/enumeratum/PlayJsonEnumSpec.scala | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala index a867884c..f416ccb2 100644 --- a/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala +++ b/enumeratum-play-json/src/test/scala/enumeratum/PlayJsonEnumSpec.scala @@ -1,19 +1,12 @@ package enumeratum +import org.scalatest.Inside import org.scalatest.OptionValues._ import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -import play.api.libs.json.{ - JsError, - JsNumber, - JsString, - JsSuccess, - JsonValidationError, - Writes, - Json => PlayJson -} +import play.api.libs.json.{Json => PlayJson, _} -class PlayJsonEnumSpec extends AnyFunSpec with Matchers { +class PlayJsonEnumSpec extends AnyFunSpec with Matchers with Inside { describe("JSON serdes") { @@ -80,10 +73,13 @@ class PlayJsonEnumSpec extends AnyFunSpec with Matchers { it("should fail with detailed error message") { val jsError = JsString("Subtract").validate[Operation] jsError shouldBe an[JsError] - jsError.asInstanceOf[JsError].errors.head._2.head shouldBe JsonValidationError( - List("error.expected.validenumvalue"), - "valid enum values are: (Eq, Add, Not), but provided: Subtract" - ) + + inside(jsError) { case JsError((_, jve :: Nil) :: Nil) => + jve shouldBe JsonValidationError( + List("error.expected.validenumvalue"), + "valid enum values are: (Eq, Add, Not), but provided: Subtract" + ) + } } } }