diff --git a/play-json-ops-common/src/main/scala/play/api/libs/json/ops/FormatOps.scala b/play-json-ops-common/src/main/scala/play/api/libs/json/ops/FormatOps.scala index 8cc972c..5729be7 100644 --- a/play-json-ops-common/src/main/scala/play/api/libs/json/ops/FormatOps.scala +++ b/play-json-ops-common/src/main/scala/play/api/libs/json/ops/FormatOps.scala @@ -8,6 +8,24 @@ import scala.util.control.NonFatal object FormatOps { + /** + * Extracts the class name of the [[Enumeration]] removing any `$` symbols without throwing an exception. + * + * @note this will include any outer object or class names separated by `.`s + */ + def enumClassName(o: Enumeration): String = { + // This logic is designed to be robust without much noise + // 1. use getName to avoid runtime exceptions from getSimpleName + // 2. filter out '$' anonymous class / method separators + // 3. start the full class name from the first upper-cased outer class name + // (to avoid picking up unnecessary package names) + o.getClass.getName + .split('.') + .last // safe because Class names will never be empty in any realistic scenario + .split('$') + .mkString(".") + } + /** * Creates a Format for the given enumeration's values by converting it to and from a string. * @@ -29,7 +47,9 @@ object FormatOps { override def reads(json: JsValue): JsResult[E#Value] = json.validate[String].flatMap { s => try JsSuccess(o.withName(s)) catch { - case NonFatal(e) => JsError(e.getMessage) + case NonFatal(_) => + val lowerCaseClassName = enumClassName(o).toLowerCase + JsError(s"error.expected.$lowerCaseClassName: No value found for '$s'") } } override def writes(o: E#Value): JsValue = JsString(o.toString) diff --git a/play-json-tests-common/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala b/play-json-tests-common/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala index cd5dd48..30c4c90 100644 --- a/play-json-tests-common/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala +++ b/play-json-tests-common/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala @@ -62,7 +62,7 @@ class FormatOpsSpec extends WordSpec { } "fails to read an invalid value" in { - assertResult(JsError("No value found for 'ERROR'")) { + assertResult(JsError("error.expected.enumexample: No value found for 'ERROR'")) { formatEnumString.reads(JsString("ERROR")) } } diff --git a/play27-json-ops-scala213/src/main/scala/play/api/libs/json/ops/FormatOps.scala b/play27-json-ops-scala213/src/main/scala/play/api/libs/json/ops/FormatOps.scala index 07c9354..e5d056f 100644 --- a/play27-json-ops-scala213/src/main/scala/play/api/libs/json/ops/FormatOps.scala +++ b/play27-json-ops-scala213/src/main/scala/play/api/libs/json/ops/FormatOps.scala @@ -2,12 +2,29 @@ package play.api.libs.json.ops import play.api.libs.json._ -import scala.language.higherKinds import scala.reflect._ import scala.util.control.NonFatal object FormatOps { + /** + * Extracts the class name of the [[Enumeration]] removing any `$` symbols without throwing an exception. + * + * @note this will include any outer object or class names separated by `.`s + */ + def enumClassName(o: Enumeration): String = { + // This logic is designed to be robust without much noise + // 1. use getName to avoid runtime exceptions from getSimpleName + // 2. filter out '$' anonymous class / method separators + // 3. start the full class name from the first upper-cased outer class name + // (to avoid picking up unnecessary package names) + o.getClass.getName + .split('.') + .last // safe because Class names will never be empty in any realistic scenario + .split('$') + .mkString(".") + } + /** * Creates a Format for the given enumeration's values by converting it to and from a string. * @@ -29,7 +46,9 @@ object FormatOps { override def reads(json: JsValue): JsResult[E#Value] = json.validate[String].flatMap { s => try JsSuccess(o.withName(s)) catch { - case NonFatal(e) => JsError(e.getMessage) + case NonFatal(_) => + val lowerCaseClassName = enumClassName(o).toLowerCase + JsError(s"error.expected.$lowerCaseClassName: No value found for '$s'") } } override def writes(o: E#Value): JsValue = JsString(o.toString) diff --git a/play27-json-tests-sc14/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala b/play27-json-tests-sc14/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala index 53f4ca6..cc0e6bf 100644 --- a/play27-json-tests-sc14/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala +++ b/play27-json-tests-sc14/src/test/scala/play/api/libs/json/ops/FormatOpsSpec.scala @@ -42,7 +42,7 @@ class FormatOpsSpec extends AnyWordSpec { } "fails to read an invalid value" in { - assertResult(JsError("No value found for 'ERROR'")) { + assertResult(JsError("error.expected.enumexample: No value found for 'ERROR'")) { formatEnumString.reads(JsString("ERROR")) } }