Переписать вложенный поиск в JSValue

Я написал следующую функцию, которая для меня немного похожа на JAVA, я хотел преобразовать ее в более Scala-способ.

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

Функция получает в качестве входных данных JsValue и имя, например name: a.b.c.d[0].e, и находит поле JsValue, которое соответствует этому ключу во вложенных, например:

{
    "a": {
        "b": {
            "c": {
                "d": [
                    {
                        "firstElement": {
                            "e": "foundValue!"
                        }
                    }
                ]
            }
        }
    }
}

Другая идея была с .split, и я также, вероятно, думаю, что есть лучший способ справиться с этой проблемой, реализация ниже:

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

Любые идеи о том, как переписать этот код в функционально правильном стиле scala? Спасибо!

ПРИМЕЧАНИЕ. Я использую библиотеки scala 2.12.8 и play-json.


Спасибо @Tomer Shetah Функция обновлена ​​до:

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

person Zvi Mints    schedule 31.01.2021    source источник


Ответы (1)


Ты можешь сделать:

val jsPath = JsPath \ "a" \ "b" \ "c" \ "d" \\ "firstElement" \ "e"
val json = Json.parse(jsonString)

println(jsPath(json))

Это приведет к:

List("foundValue!")

Обратите внимание, что \\ получит все элементы массива. Например, если в массиве в d будет одноуровневый элемент:

{
    "firstElement": {
        "e": "foundValue!"
    }
}

чем результат будет:

List("foundValue!", "foundValue!2")

Вы запрашиваете первый элемент, поэтому, чтобы получить его, вы можете сделать:

jsPath(json).headOption

Если вам нужно преобразовать путь "a.b.c.d[0].firstElement.e" в JsPath, вы можете сделать:

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

Затем вызов:

val jsPath = JsPath(path)
val json = Json.parse(jsonString)
println(jsPath(json))

Будет получено с:

List("foundValue!")

Код запускается по адресу Scastie.

person Tomer Shetah    schedule 31.01.2021
comment
Кстати, вы также можете сделать: val jsPath = JsPath \ "a" \ "b" \ "c" \ "d" \ 0 \ "firstElement" \ "e" - person Tomer Shetah; 31.01.2021
comment
Удивительно! спасибо за помощь @Tomer shetah :) - кстати, есть ли преимущества использования JsPath по сравнению со старой рекурсией? (производительность и т.д.) - person Zvi Mints; 31.01.2021
comment
Я не думаю, что есть большая разница в производительности между двумя вариантами. Во-первых, масштаб очень маленький. Во-вторых, в вашем подходе вы много раз используете регулярные выражения. Что не очень эффективно. Таким образом, возможно, что вы улучшите производительность с помощью хвостовой рекурсии, но я предполагаю, что улучшение будет незначительным. - person Tomer Shetah; 31.01.2021
comment
Отлично работает, спасибо @Tomer Shetah - person Zvi Mints; 05.02.2021