vital-software/scala-redox

View on GitHub
src/main/scala/com/github/vitalsoftware/util/RobustParsing.scala

Summary

Maintainability
A
1 hr
Test Coverage
package com.github.vitalsoftware.util

import play.api.libs.json._

/**
 * We may fail to parse certain fields of a redox Json message to our domain object. However, we still need to extract
 * useful fields from the message to continue processing, Robust parsing will recursively parse and then remove any invalid
 * fields until a result can be obtained.
 *
 * First we try to parse the passed in Json object, if there are any errors we trim the heads of the paths containing
 * the errors and try to parse again. This allows us to assign defaults to any fields that cannot be parsed if they are
 * defined and try to get a valid result.
 *
 * This returns a tuple of (maybeErrors, maybeResult) so nessorory action can be taken downstream about and recovered errors.
 * ie: Logging recovery errors to fix if needed.
 */
object RobustParsing {
  // Todo: Make this tail recursive
  //  @scala.annotation.tailrec
  def robustParsing[A](reads: Reads[A], json: JsValue, seen: Seq[JsPath] = Nil): (Option[JsError], Option[A]) =
    json
      .validate(reads)
      .fold(
        invalid = { errors =>
          val paths = errors.map(_._1)

          // If this field is already seen, trim the last node in the path
          val trimmedPaths = paths
            .intersect(seen)
            .map(p => p.path.splitAt(p.path.length - 2))
            .map {
              // if the last node is an element in an array, remove the entire array
              case (beforeArray, IdxPathNode(_) :: _) =>
                beforeArray
              // otherwise drop the last node in path
              case (before, after) => (before ::: after).dropRight(1)
            }
            .filter(_.nonEmpty)
            .map(JsPath.apply)

          val unseen = paths.diff(seen)
          val pathsToPrune = trimmedPaths ++ unseen

          if (pathsToPrune.nonEmpty) {
            val transforms = pathsToPrune
              .map(prune)
              .reduce(_ andThen _)

            val pruned = json.transform(transforms)

            RobustParsing.robustParsing(reads, pruned.get, seen ++ pathsToPrune) match {
              case (_, maybeResult) => (Some(JsError(errors)), maybeResult)
            }
          } else {
            (Some(JsError(errors)), None)
          }
        },
        valid = res => (None, Some(res))
      )

  def prune(path: JsPath): Reads[_ >: JsObject <: JsValue] = {
    val index = path.path.indexWhere(_.isInstanceOf[IdxPathNode])
    val splitIndex = if (index == -1) Int.MaxValue else index
    path.path.splitAt(splitIndex) match {
      // primitive array, just return the value
      case (Nil, Nil) => path.json.pick
      // no arrays, safe to prune
      case (path, Nil) => JsPath(path).json.prune
      // encountered an array JsValue, prune each element
      case (path, IdxPathNode(idx) :: tail) => prunePathInArray(JsPath(path), idx, JsPath(tail))
      // unhandled, just return the value
      case _ => path.json.pick
    }
  }

  def prunePathInArray(toArrayElement: JsPath, index: Int, afterArray: JsPath): Reads[_ >: JsObject <: JsValue] =
    toArrayElement.path match {
      case Nil =>
        Reads.of[JsArray].map {
          case JsArray(arr) => JsArray(arr.updated(index, arr(index).transform(prune(afterArray)).get))
        }
      case _ =>
        toArrayElement.json.update(
          Reads.of[JsArray].map {
            case JsArray(arr) => JsArray(arr.updated(index, arr(index).transform(prune(afterArray)).get))
          }
        )
    }
}