Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KeyReads/Writes instances #579

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions play-json/shared/src/main/scala/play/api/libs/json/KeyReads.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,73 @@

package play.api.libs.json

import scala.util.control.NonFatal

/**
* Used to read object key for types other than `String`.
*
* @see [[Reads.keyMapReads]]
*/
trait KeyReads[T] {
trait KeyReads[T] { self =>
def readKey(key: String): JsResult[T]

final def map[U](f: T => U): KeyReads[U] = KeyReads[U] { key =>
self.readKey(key).map(f)
}
}

object KeyReads extends EnvKeyReads {
object KeyReads extends EnvKeyReads with LowPriorityKeyReads {

/**
* Returns an instance which uses `f` as [[KeyReads.readKey]] function.
*/
def apply[T](f: String => JsResult[T]): KeyReads[T] = new KeyReads[T] {
def readKey(key: String) = f(key)
}

implicit val charKeyReads: KeyReads[Char] = KeyReads[Char] {
_.headOption match {
case Some(ch) => JsSuccess(ch)
case _ => JsError("error.expected.character")
}
}

implicit val booleanKeyReads: KeyReads[Boolean] = KeyReads[Boolean] {
case "true" => JsSuccess(true)
case "false" => JsSuccess(false)
case _ => JsError("error.expected.boolean")
}

implicit val byteKeyReads: KeyReads[Byte] = charKeyReads.map(_.toByte)

implicit val shortKeyReads: KeyReads[Short] =
unsafe[Short]("error.expected.short")(_.toShort)

implicit val intKeyReads: KeyReads[Int] =
unsafe[Int]("error.expected.int")(_.toInt)

implicit val longKeyReads: KeyReads[Long] =
unsafe[Long]("error.expected.long")(_.toLong)

implicit val floatKeyReads: KeyReads[Float] =
unsafe[Float]("error.expected.float")(_.toFloat)

implicit val doubleKeyReads: KeyReads[Double] =
unsafe[Double]("error.expected.double")(_.toDouble)

private def unsafe[T](err: String)(f: String => T): KeyReads[T] =
KeyReads[T] { key =>
try {
JsSuccess(f(key))
} catch {
case NonFatal(_) => JsError(err)
}
}
}

private[json] sealed trait LowPriorityKeyReads {
implicit def readableKeyReads[T](implicit r: Reads[T]): KeyReads[T] =
KeyReads[T] { key =>
r.reads(JsString(key))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ object KeyWrites extends EnvKeyWrites {
def apply[T](f: T => String): KeyWrites[T] = new KeyWrites[T] {
def writeKey(key: T) = f(key)
}

implicit def anyValKeyWrites[T <: AnyVal]: KeyWrites[T] =
KeyWrites[T](_.toString)
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside {
"be read with character keys".which {
"are characters" in {
Json
.fromJson[Map[Char, Int]](Json.obj("a" -> 1, "b" -> 2))(Reads.charMapReads)
.fromJson[Map[Char, Int]](Json.obj("a" -> 1, "b" -> 2))
.mustEqual(JsSuccess(Map('a' -> 1, 'b' -> 2)))
}

Expand All @@ -92,6 +92,71 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside {
)
}
}

"be read with boolean keys".which {
"are booleans" in {
Json
.fromJson[Map[Boolean, String]](Json.obj("true" -> "foo", "false" -> "bar"))
.mustEqual(JsSuccess(Map[Boolean, String](true -> "foo", false -> "bar")))
}

"are not booleans" in {
Json
.fromJson[Map[Boolean, String]](Json.obj("foo" -> "", "bar" -> ""))
.mustEqual(
JsError(
List(
(JsPath \ "foo", List(JsonValidationError("error.expected.boolean"))),
(JsPath \ "bar", List(JsonValidationError("error.expected.boolean")))
)
)
)
}
}

"be read with byte keys" in {
Json
.fromJson[Map[Byte, Int]](Json.obj("a" -> 1, "b" -> 2))
.mustEqual(JsSuccess(Map('a'.toByte -> 1, 'b'.toByte -> 2)))
}

"be read with short keys" in {
Json
.fromJson[Map[Short, Int]](Json.obj("1" -> 1, "2" -> 2))
.mustEqual(JsSuccess(Map(1.toByte -> 1, 2.toByte -> 2)))
}

"be read with int keys" in {
Json
.fromJson[Map[Int, String]](Json.obj("1" -> "foo", "2" -> "bar"))
.mustEqual(JsSuccess(Map(1 -> "foo", 2 -> "bar")))
}

"be read with long keys" in {
Json
.fromJson[Map[Long, String]](Json.obj("1" -> "foo", "2" -> "bar"))
.mustEqual(JsSuccess(Map(1L -> "foo", 2L -> "bar")))
}

"be read with float keys" in {
Json
.fromJson[Map[Float, String]](Json.obj("1.23" -> "foo", "23.4" -> "bar"))
.mustEqual(JsSuccess(Map(1.23F -> "foo", 23.4F -> "bar")))
}

"be read with double keys" in {
Json
.fromJson[Map[Double, String]](Json.obj("1.23" -> "foo", "23.4" -> "bar"))
.mustEqual(JsSuccess(Map(1.23D -> "foo", 23.4D -> "bar")))
}

"be read with Reads'able keys" in {
val key = "https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.libs.json.JsResult"

implicitly[KeyReads[URI]]

Json.fromJson[Map[URI, String]](Json.obj(key -> "foo")).mustEqual(JsSuccess(Map((new URI(key)) -> "foo")))
}
}

"Compose" should {
Expand Down Expand Up @@ -225,7 +290,7 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside {

"URI" should {
"be read from JsString" in {
val strRepr = "https://www.playframework.com/documentation/2.6.x/api/scala/index.html#play.api.libs.json.JsResult"
val strRepr = "https://www.playframework.com/documentation/2.8.x/api/scala/index.html#play.api.libs.json.JsResult"

JsString(strRepr).validate[URI].mustEqual(JsSuccess(new URI(strRepr)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class WritesSharedSpec extends AnyWordSpec with Matchers {

"Iterable writes" should {
"write maps" in {
Json.toJson(Map(1 -> "one")).mustEqual(Json.arr(Json.arr(1, "one")))
Json.toJson(Map(1 -> "one")).mustEqual(Json.obj("1" -> "one"))
}
}

Expand Down