I wrote the following function - which a little bit of JAVA way for me, I wanted to convert it to a more Scala way
@tailrec
private[services] final def nestedSearch(payload: JsValue, name: String): JsLookupResult = {
val arrRegex = "([\\w]+)\\[([\\d]+)\\]".r
def currLevelSearch(payload: JsValue, name: String): JsLookupResult = {
name match {
case arrRegex(arr, index) if Try(index.toInt).isSuccess => payload \ arr \ index.toInt // name is from the form arr[index], NOTE: by regex index is decimal => index.toInt is safe but still double-check
case _ => payload \ name
}
}
name indexOf "." match {
case lastSearch if lastSearch < 0 => currLevelSearch(payload, name)
case currSearch if currSearch >= 0 =>
val `until first .` = name.substring(0, currSearch)
val `after first .` = name.substring(currSearch + 1)
currLevelSearch(payload, `until first .`) match {
case JsDefined(newPayload) => nestedSearch(newPayload, `after first .`)
case undefined: JsUndefined => undefined
}
}
}
The function gets as a input JsValue and name, for instance name: a.b.c.d[0].e
and its find the JsValue field that matchs this key in nested, for example:
{
"a": {
"b": {
"c": {
"d": [
{
"firstElement": {
"e": "foundValue!"
}
}
]
}
}
}
}
Other idea was with .split
which i also probably think there some better way to handle this issue, the implementation is bellow:
private[services] final def nestedSearch(payload: JsValue, name: String): JsLookupResult = {
val arrRegex = "([\\w]+)\\[([\\d]+)\\]".r
@tailrec def nestedSearchRec(keys: List[String])(seed: JsLookupResult): JsLookupResult = keys match {
// Base cases:
case Nil => seed
case _ if seed.isEmpty => seed
// Step case:
case name :: tail =>
nestedSearchRec(tail) {
name match {
case arrRegex(arr, index) => seed \ arr \ index.toInt
case name => seed \ name
}
}
}
nestedSearchRec(name.split('.').toList)(JsDefined(payload))
}
Any ideas on how to re-write this code in functional proper scala way? Thanks!
NOTE: Im using scala 2.12.8
and play-json
library
Thanks to @Tomer Shetah The function upgraded to:
private[services] final def nestedSearch(payload: JsValue, name: String): JsLookupResult = {
val arrRegex = "([\\w]+)\\[([\\d]+)\\]".r
JsPath {
name.split("\\.").toList.flatMap {
case arrRegex(node, index) => List(KeyPathNode(node), IdxPathNode(index.toInt))
case s => List(KeyPathNode(s))
}
}(payload) match {
case Nil => JsUndefined(s"Could not find $name in $payload")
case res :: _ => JsDefined(res)
}
}
You can do:
val jsPath = JsPath \ "a" \ "b" \ "c" \ "d" \\ "firstElement" \ "e"
val json = Json.parse(jsonString)
println(jsPath(json))
This will result with:
List("foundValue!")
Please note that \\
will get all elements in the array. For example if in the array in d
there will be a sibling:
{
"firstElement": {
"e": "foundValue!"
}
}
than the result will be:
List("foundValue!", "foundValue!2")
You ask for the first element, so to get it you can do:
jsPath(json).headOption
In case you need to convert the path "a.b.c.d[0].firstElement.e"
into JsPath
, you can do:
val path: List[PathNode] = pathString.split("\\.").toList.flatMap {
case s"$node[$index]" if index.toIntOption.isDefined =>
List(KeyPathNode(node), IdxPathNode(index.toInt))
case s=>
List(KeyPathNode(s))
}
Then calling:
val jsPath = JsPath(path)
val json = Json.parse(jsonString)
println(jsPath(json))
Will result with:
List("foundValue!")
Code run at Scastie.