4

I dynamically generate a bunch of Reads[JsObject] which I then have in a Seq[Reads[JsObject]]. In order to actually apply all these single Reads[JsObject], I've got to merge them with and into one single Reads[JsObject]. Is this possible?

I've got (example):

val generatedReads: Seq[Reads[JsObject]] = Seq(
    (__ \ "attr1").json.copyFrom((__ \ "attr1" \ "attr1a").json.pick),
    (__ \ "attr2").json.pickBranch
)

What I need:

val finalReads: Reads[JsObject] =
    (__ \ "attr1").json.copyFrom((__ \ "attr1" \ "attr1a").json.pick) and
    (__ \ "attr2").json.pickBranch

The property names and which branch to pick is not known at compile time, that's why it's got to be dynamic.

andrey.ladniy
  • 1,664
  • 1
  • 11
  • 27
Nick
  • 2,576
  • 1
  • 23
  • 44

1 Answers1

4

This is a fairly common question. This answer inspired by Reads.traversableReads[F[_], A].

To support the idea of cumulative of Reads[A], we must try all your genererated Reads[JsObject] and we will use Either[Errors, Vector[JsObject]] for this. In original 'Reads.traversableReads[F[_], A]' returns Reads[List[A]] or some collection, but we need simple Json, no problem, ++ concatinates our JsObjects.

def reduceReads(generated: Seq[Reads[JsObject]]) = Reads {json =>
  type Errors = Seq[(JsPath, Seq[ValidationError])]

  def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr }

  generated.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[JsObject]]) {
    case (acc, (r, idx)) => (acc, r.reads(json)) match {
      case (Right(vs), JsSuccess(v, _)) => Right(vs :+ v)
      case (Right(_), JsError(e)) => Left(locate(e, idx))
      case (Left(e), _: JsSuccess[_]) => Left(e)
      case (Left(e1), JsError(e2)) => Left(e1 ++ locate(e2, idx))
    }
  }
    .fold(JsError.apply, { res =>
      JsSuccess(res.fold(Json.obj())(_ ++ _))
    })
}

scala> json: play.api.libs.json.JsValue = {"attr1":{"attr1a":"attr1a"},"attr2":"attr2"}

scala> res7: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = JsSuccess({"attr1":"attr1a","attr2":"attr2"},)

new awesome simple answer


After few days I had this brilliant idea. object Reads have implicit Reducer[JsObject, JsObject] so we can reduce Seq(Reads[JsObject]) with FunctionalBuilder (and and then reduce)!

def reduceReads(generated: Seq[Reads[JsObject]]) = 
  generated.foldLeft(Reads.pure(Json.obj())){
    case (acc, r) =>
      (acc and r).reduce 
  }

This solution is simple and clear. Original idea based Seq(Reads[JsObject]) => Seq(JsResult[JsObject]) => Reads[JsObject] mapping, but the last based on basic Json combinators principles Seq(Reads[JsObject]) => Reads[JsObject]


In general, the problem is solved, but the task itself is not correct. If you don't control Reads, what you want see if the same path will be used twice?

andrey.ladniy
  • 1,664
  • 1
  • 11
  • 27
  • I've got to admit, right now I don't fully understand it, but it definitely does what I wanted. I'm sure, the more I play around with the code, I'll figure it out. Thanks! – Nick Apr 26 '16 at 11:46