Search code examples
scalaplay-json

Rewrite nested search in JSValue


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)
    }
  }

Solution

  • 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.