Skip to content

Commit

Permalink
Merge pull request #896 from cchantep/feature/tuples
Browse files Browse the repository at this point in the history
Custom field names for tuples Reads/Writes
  • Loading branch information
mkurz authored Jul 16, 2023
2 parents 373fd1c + d3bfbae commit 6649729
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/manual/working/scalaGuide/main/json/ScalaJson.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,9 @@ To convert from JsValue to a model, you must define implicit `Reads[T]` where `T
@[sample-model](code/ScalaJsonSpec.scala)

@[convert-to-model](code/ScalaJsonSpec.scala)

### Using simple tuples

Simple JSON object can be reads as and writes from simple tuples.

@[handle-simple-tuples](code/ScalaJsonSpec.scala)
21 changes: 21 additions & 0 deletions docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,5 +452,26 @@ class ScalaJsonSpec extends Specification {
placeResult.must(beLike { case JsSuccess(Place(name, _, _), _) => name === "Watership Down" })
residentResult.must(beLike { case JsSuccess(Resident(name, _, _), _) => name === "Bigwig" })
}

"handle simple tuples" in {
//#handle-simple-tuples
import play.api.libs.json._

val tuple3Reads: Reads[(String, Int, Boolean)] =
Reads.tuple3[String, Int, Boolean]("name", "age", "isStudent")

val tuple3Writes: OWrites[(String, Int, Boolean)] =
OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")

val tuple3ExampleJson: JsObject =
Json.obj("name" -> "Bob", "age" -> 30, "isStudent" -> false)

val tuple3Example = Tuple3("Bob", 30, false)

tuple3Writes.writes(tuple3Example) mustEqual tuple3ExampleJson

tuple3Reads.reads(tuple3ExampleJson) mustEqual JsSuccess(tuple3Example)
//#handle-simple-tuples
}
}
}
98 changes: 98 additions & 0 deletions play-json/shared/src/main/scala/play/api/libs/json/Reads.scala
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,102 @@ trait DefaultReads extends LowPriorityDefaultReads {
}

implicit val uuidReads: Reads[java.util.UUID] = new UUIDReader(false)

/**
* Reads a JSON object and constructs a tuple of two values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @tparam A the type for the first element
* @tparam B the type for the second element
*
* {{{
* val tuple2Reads: Reads[(String, Int)] = Reads.tuple2[String, Int]("name", "age")
*
* val tuple2ExampleJson = Json.obj("name" -> "Alice", "age" -> 25)
* val tuple2Result: JsResult[(String, Int)] = tuple2Reads.reads(tuple2ExampleJson)
* // JsSuccess(("Alice", 25))
* }}}
*/
def tuple2[A: Reads, B: Reads](name1: String, name2: String): Reads[(A, B)] =
Reads[(A, B)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
} yield _1 -> _2
}

/**
* Reads a JSON object and constructs a tuple of three values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @param name3 the name of the third element `_3`
* @tparam A the type for the first element
* @tparam B the type for the second element
* @tparam C the type for the third element
*
* {{{
* val tuple3Reads: Reads[(String, Int, Boolean)] =
* Reads.tuple3[String, Int, Boolean]("name", "age", "isStudent")
*
* val tuple3ExampleJson: JsValue =
* Json.obj("name" -> "Alice", "age" -> 25, "isStudent" -> true)
*
* val tuple3Result: JsResult[(String, Int, Boolean)] =
* tuple3Reads.reads(tuple3ExampleJson)
* // JsSuccess(("Alice", 25, true))
* }}}
*/
def tuple3[A: Reads, B: Reads, C: Reads](name1: String, name2: String, name3: String): Reads[(A, B, C)] =
Reads[(A, B, C)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
_3 <- (js \ name3).validate[C]
} yield Tuple3(_1, _2, _3)
}

/**
* Reads a JSON object and constructs a tuple of four values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @param name3 the name of the third element `_3`
* @param name4 the name of the fourth element `_4`
* @tparam A the type for the first element
* @tparam B the type for the second element
* @tparam C the type for the third element
* @tparam D the type for the fourth element
*
* {{{
* val tuple4Reads: Reads[(String, Int, Boolean, Double)] =
* Reads.tuple4[String, Int, Boolean, Double](
* "name", "age", "isStudent", "score")
*
* val tuple4ExampleJson: JsValue = Json.obj(
* "name" -> "Alice", "age" -> 25, "isStudent" -> true, "score" -> 78.9)
*
* val tuple4Result: JsResult[(String, Int, Boolean, Double)] =
* tuple4Reads.reads(tuple4ExampleJson)
* // JsSuccess(("Alice", 25, true, 78.9))
* }}}
*/
def tuple4[A: Reads, B: Reads, C: Reads, D: Reads](
name1: String,
name2: String,
name3: String,
name4: String
): Reads[(A, B, C, D)] =
Reads[(A, B, C, D)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
_3 <- (js \ name3).validate[C]
_4 <- (js \ name4).validate[D]
} yield Tuple4(_1, _2, _3, _4)
}
}
71 changes: 71 additions & 0 deletions play-json/shared/src/main/scala/play/api/libs/json/Writes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,77 @@ object OWrites extends PathWrites with ConstraintWrites {
OWrites[A] { a =>
f(a, w.writes(a))
}

/**
* Writes a tuple of two values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @tparam A the type of the first value
* @tparam B the type of the second value
*
* {{{
* val tuple2Writes: OWrites[(String, Int)] =
* OWrites.tuple2[String, Int]("name", "age")
*
* tuple2Writes.writes("Bob" -> 30) // {"name":"Bob","age":30}
* }}}
*/
def tuple2[A: Writes, B: Writes](name1: String, name2: String): OWrites[(A, B)] = OWrites[(A, B)] { case (a, b) =>
Json.obj(name1 -> a, name2 -> b)
}

/**
* Writes a tuple of three values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @param name3 the name of the third field
* @tparam A the type of the first value
* @tparam B the type of the second value
* @tparam C the type of the third value
*
* {{{
* val tuple3Writes: OWrites[(String, Int, Boolean)] =
* OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")
*
* tuple3Writes.writes(("Bob", 30, false))
* // {"name":"Bob","age":30,"isStudent":false}
* }}}
*/
def tuple3[A: Writes, B: Writes, C: Writes](name1: String, name2: String, name3: String): OWrites[(A, B, C)] =
OWrites[(A, B, C)] { case (a, b, c) =>
Json.obj(name1 -> a, name2 -> b, name3 -> c)
}

/**
* Writes a tuple of four values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @param name3 the name of the third field
* @param name4 the name of the fourth field
* @tparam A the type of the first value
* @tparam B the type of the second value
* @tparam C the type of the third value
* @tparam D the type of the fourth value
*
* {{{
* val tuple4Writes: OWrites[(String, Int, Boolean, Double)] =
* OWrites.tuple4[String, Int, Boolean, Double]("name", "age", "isStudent", "score")
*
* tuple4Writes.writes(("Bob", 30, false, 91.2))
* // {"name":"Bob","age":30,"isStudent":false,"score":91.2}
* }}}
*/
def tuple4[A: Writes, B: Writes, C: Writes, D: Writes](
name1: String,
name2: String,
name3: String,
name4: String
): OWrites[(A, B, C, D)] = OWrites[(A, B, C, D)] { case (a, b, c, d) =>
Json.obj(name1 -> a, name2 -> b, name3 -> c, name4 -> d)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,34 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside {
}
}

"Tuple" should {
"be read with custom element names" when {
"tuple2" in {
val reads = Reads.tuple2[String, Float]("name", "score")

Json.obj("name" -> "Foo", "score" -> 1.23F).validate(reads).mustEqual(JsSuccess("Foo" -> 1.23F))
}

"tuple3" in {
val reads = Reads.tuple3[String, Float, Int]("name", "score", "age")

Json
.obj("name" -> "Foo", "age" -> 10, "score" -> 1.23F)
.validate(reads)
.mustEqual(JsSuccess(Tuple3("Foo", 1.23F, 10)))
}

"tuple4" in {
val reads = Reads.tuple4[String, Float, Int, Seq[String]]("name", "score", "age", "aliases")

Json
.obj("name" -> "Foo", "aliases" -> Seq("Bar"), "age" -> 10, "score" -> 1.23F)
.validate(reads)
.mustEqual(JsSuccess(Tuple4("Foo", 1.23F, 10, Seq("Bar"))))
}
}
}

"Identity reads" should {
def success[T <: JsValue](fixture: T)(implicit r: Reads[T], ct: scala.reflect.ClassTag[T]) =
s"be resolved for $fixture as ${ct.runtimeClass.getSimpleName}" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ final class WritesSharedSpec extends AnyWordSpec with Matchers {
}
}

"Tuples" should {
"be written with custom field names" when {
"tuple2" in {
val w = OWrites.tuple2[String, Double]("name", "score")

w.writes("Foo" -> 23.4D).mustEqual(Json.obj("name" -> "Foo", "score" -> 23.4D))
}

"tuple3" in {
val w = OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")

w.writes(("Alice", 25, true)).mustEqual(Json.obj("name" -> "Alice", "age" -> 25, "isStudent" -> true))
}

"tuple4" in {
val w = OWrites.tuple4[String, Int, Boolean, Double]("name", "age", "isStudent", "score")

w.writes(("Bob", 30, false, 78.9D))
.mustEqual(Json.obj("name" -> "Bob", "age" -> 30, "isStudent" -> false, "score" -> 78.9D))
}
}
}

"Identity writes" should {
import scala.reflect.ClassTag
import scala.language.higherKinds
Expand Down

0 comments on commit 6649729

Please sign in to comment.