All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itv.scalapact.shared.matchir.IrNodeEqualityResult.scala Maven / Gradle / Ivy

package com.itv.scalapact.shared.matchir

import com.itv.scalapact.shared.matchir.IrNodeEqualityResult.{IrNodesEqual, IrNodesNotEqual}

import scala.annotation.tailrec

sealed abstract class IrNodeEqualityResult(val isEqual: Boolean) {
  def +(other: IrNodeEqualityResult): IrNodeEqualityResult =
    (this, other) match {
      case (IrNodesEqual, IrNodesEqual)               => IrNodesEqual
      case (IrNodesEqual, r @ IrNodesNotEqual(_))     => r
      case (l @ IrNodesNotEqual(_), IrNodesEqual)     => l
      case (IrNodesNotEqual(d1), IrNodesNotEqual(d2)) => IrNodesNotEqual(d1 ++ d2)
    }

  def renderAsString: String =
    this match {
      case IrNodesEqual =>
        "Nodes equal"

      case n: IrNodesNotEqual =>
        n.renderDifferences
    }

}

object IrNodeEqualityResult {

  case object IrNodesEqual extends IrNodeEqualityResult(true)

  final case class IrNodesNotEqual(differences: List[IrNodeDiff]) extends IrNodeEqualityResult(false) {
    def renderDifferencesListWithRules(rules: IrNodeMatchingRules, isXml: Boolean): List[String] =
      differences
        .groupBy(_.path.renderAsString)
        .map { k =>
          val path = k._2.headOption.map(_.path).getOrElse(IrNodePath.empty)
          val relevantRules = IrNodeMatchingRules(rules.findForPath(path, isXml), RuleProcessTracing.disabled) + rules
            .findAncestralTypeRule(path, isXml)

          s"""Node at: ${k._1}
             |  ${k._2.map(_.message).mkString("\n  ")}
             |
             |> Rules:
             |${relevantRules.renderAsString}
             |
       """.stripMargin
        }
        .toList

    def renderDifferencesList: List[String] =
      differences
        .groupBy(_.path.renderAsString)
        .map { k =>
          s"""Node at: ${k._1}
             |  ${k._2.map(_.message).mkString("\n  ")}
       """.stripMargin
        }
        .toList

    def renderDifferences: String =
      renderDifferencesList.mkString("\n")
  }

  final case class IrNodeDiff(message: String, path: IrNodePath)

  object IrNodesNotEqual {
    def apply(message: String, path: IrNodePath): IrNodesNotEqual = IrNodesNotEqual(List(IrNodeDiff(message, path)))
  }

  val nodeType: (IrNodePath, Boolean) => (Boolean, Boolean) => IrNodeEqualityResult =
    (path, isXml) =>
      (a, b) =>
        if (isXml) IrNodesEqual
        else {
          val f = (bb: Boolean) => if (bb) "array" else "object"

          if (a == b) IrNodesEqual
          else IrNodesNotEqual(s"Expected type '${f(a)}' but got '${f(b)}'", path)
        }

  val labelTest: IrNodePath => (String, String) => IrNodeEqualityResult =
    path =>
      (a, b) => {
        if (a == b) IrNodesEqual else IrNodesNotEqual(s"Label '$a' did not match '$b'", path)
      }

  val valueTest: (Boolean, Boolean, IrNodePath, IrNodeMatchingRules, IrNode, IrNode) => (
      Option[IrNodePrimitive],
      Option[IrNodePrimitive]
  ) => IrNodeEqualityResult = { (strict, isXml, path, rules, parentA, parentB) => (a, b) =>
    if (parentA.path.lastSegmentLabel == parentB.path.lastSegmentLabel) {
      val equality: IrNodeEqualityResult = if (strict) {
        (a, b) match {
          case (Some(v1: IrNodePrimitive), Some(v2: IrNodePrimitive)) =>
            if (v1 == v2) IrNodesEqual
            else IrNodesNotEqual(s"Value '${v1.renderAsString}' did not match '${v2.renderAsString}'", path)

          case (Some(v1: IrNodePrimitive), None) =>
            IrNodesNotEqual(s"Value '${v1.renderAsString}' did not match empty value", path)

          case (None, Some(v2: IrNodePrimitive)) =>
            IrNodesNotEqual(s"Empty value did not match '${v2.renderAsString}'", path)

          case (None, None) =>
            IrNodesEqual
        }
      } else {
        (a, b) match {
          case (Some(v1: IrNodePrimitive), Some(v2: IrNodePrimitive)) =>
            if (v1 == v2) IrNodesEqual
            else IrNodesNotEqual(s"Value '${v1.renderAsString}' did not match '${v2.renderAsString}'", path)

          case (Some(v1: IrNodePrimitive), None) =>
            IrNodesNotEqual(s"Value '${v1.renderAsString}' did not match empty value", path)

          case (None, Some(_: IrNodePrimitive)) =>
            IrNodesEqual

          case (None, None) =>
            IrNodesEqual
        }
      }

      RuleChecks.checkForPrimitive(rules, path, a, b, checkParentTypeRule = true, isXml).getOrElse(equality)
    } else {
      IrNodesEqual
    }
  }

  val namespaceTest: IrNodePath => (Option[String], Option[String]) => IrNodeEqualityResult = path => {
    case (Some(v1: String), Some(v2: String)) =>
      if (v1 == v2) IrNodesEqual else IrNodesNotEqual(s"Namespace '$v1' did not match '$v2'", path)

    case (Some(v1: String), None) =>
      IrNodesNotEqual(s"Namespace '$v1' did not match empty namespace", path)

    case (None, Some(v2: String)) =>
      IrNodesNotEqual(s"Empty namespace did not match '$v2'", path)

    case (None, None) =>
      IrNodesEqual
  }

  val pathTest: IrNodePath => (IrNodePath, IrNodePath) => IrNodeEqualityResult = path =>
    (a, b) => {
      val segA = a.lastSegmentLabel
      val segB = b.lastSegmentLabel

      if (segA == segB) IrNodesEqual
      else IrNodesNotEqual(s"Path node '$segA' did not match '$segB'", path)
    }

  private def foldResults(l: List[IrNodeEqualityResult]): IrNodeEqualityResult =
    l match {
      case Nil     => IrNodesEqual
      case x :: xs => xs.foldLeft(x)(_ + _)
    }

  private def strictCheckChildren(
      path: IrNodePath,
      strict: Boolean,
      bePermissive: Boolean,
      rules: IrNodeMatchingRules,
      a: List[IrNode],
      b: List[IrNode],
      ignoreLength: Boolean
  ): IrNodeEqualityResult =
    if (!ignoreLength && a.length != b.length)
      IrNodesNotEqual(
        s"Differing number of children, cannot check equality. Expected ${a.length} got ${b.length}",
        path
      )
    else {
      foldResults(a.zip(b).map { p =>
        p._1.isEqualTo(p._2, strict, rules, bePermissive)
      })
    }

  private def findClosestMatch(
      expected: IrNode,
      possibleMatches: List[IrNode],
      strict: Boolean,
      rules: IrNodeMatchingRules,
      bePermissive: Boolean
  ): IrNodeEqualityResult = {
    @tailrec
    def rec(e: IrNode, remaining: List[IrNode], fails: List[IrNodesNotEqual]): IrNodeEqualityResult =
      remaining match {
        case Nil =>
          fails.sortBy(_.differences.length).headOption match {
            // Should not happen, types are lying...
            case None =>
              IrNodesNotEqual(s"Could not find a match for node", e.path)

            case Some(d) =>
              d
          }

        case a :: as =>
          e.isEqualTo(a, strict, rules, bePermissive) match {
            case success: IrNodesEqual.type =>
              success

            case failure @ IrNodesNotEqual(_) =>
              rec(e, as, failure :: fails)
          }
      }

    rec(expected, possibleMatches, Nil)
  }

  private def permissiveCheckChildren(
      strict: Boolean,
      bePermissive: Boolean,
      rules: IrNodeMatchingRules,
      a: List[IrNode],
      b: List[IrNode]
  ): IrNodeEqualityResult =
    foldResults(a.map { actual =>
      findClosestMatch(actual, b, strict, rules, bePermissive)
    })

  val childrenTest: (Boolean, IrNodePath, Boolean, Boolean, IrNodeMatchingRules, IrNode, IrNode) => (
      List[IrNode],
      List[IrNode]
  ) => IrNodeEqualityResult =
    (strict, path, isXml, bePermissive, rules, parentA, parentB) =>
      (a, b) => {
        if (strict) {
          if (a.length < b.length && (!bePermissive || (bePermissive && parentA.isArray && !isXml))) {
            val parentCheck: Option[IrNodeEqualityResult] =
              RuleChecks.checkForNode(
                rules.findMinArrayLengthRule(path, parentA.isXml),
                path,
                parentA,
                parentB
              )

            val childrenCheck: IrNodeEqualityResult = parentCheck match {
              case Some(pc) if pc.isEqual =>
                // If there was a min array length rule (and we know the arrays are unbalanced)
                // then we need to populate an 'A' array to compare with 'B' so that we check
                // everything correctly, running on the assumption that at this point, the user
                // is relying on rules to validate because frankly all bets for normal comparison
                // are off!
                val newA = b.map(_ => a.headOption).collect { case Some(s) => s }

                strictCheckChildren(path, strict, bePermissive, rules, newA, b, ignoreLength = false)

              case _ =>
                RuleChecks
                  .checkForNode(rules, path, parentA, parentB)
                  .getOrElse {
                    strictCheckChildren(path, strict, bePermissive, rules, a, b, ignoreLength = false)
                  }
            }

            parentCheck.map(p => p + childrenCheck).getOrElse(childrenCheck)

          } else if (a.length < b.length && bePermissive && isXml) {
            val newA = b.map(_ => a.headOption).collect { case Some(s) => s }

            val maybeResult: Option[IrNodeEqualityResult] = newA
              .zip(b)
              .map { p =>
                RuleChecks.checkForNode(rules, p._2.path, p._1, p._2)
              }
              .collect { case Some(d @ IrNodesNotEqual(_)) =>
                d
              } match {
              case Nil =>
                None

              case x :: xs =>
                Option(xs.foldLeft[IrNodeEqualityResult](x)(_ + _))
            }

            val equalityResult = strictCheckChildren(path, strict, bePermissive, rules, a, b, ignoreLength = true)

            maybeResult.getOrElse(equalityResult)

          } else if (parentA.isArray && a.length == b.length) {
            strictCheckChildren(path, strict, bePermissive, rules, a, b, ignoreLength = false)
          } else if (isXml) {
            strictCheckChildren(path, strict, bePermissive, rules, a, b, ignoreLength = false)
          } else {
            permissiveCheckChildren(strict, bePermissive, rules, a, b)
          }
        } else {
          val newA = b.map(_ => a.headOption).collect { case Some(s) => s }

          val maybeResult: Option[IrNodeEqualityResult] = newA
            .zip(b)
            .map { p =>
              RuleChecks.checkForNode(rules, p._2.path, p._1, p._2)
            }
            .collect { case Some(d @ IrNodesNotEqual(_)) =>
              d
            } match {
            case Nil =>
              None

            case x :: xs =>
              Option(xs.foldLeft[IrNodeEqualityResult](x)(_ + _))
          }

          val equalityResult = permissiveCheckChildren(strict, bePermissive, rules, a, b)

          maybeResult.getOrElse(equalityResult)
        }
      }

  private val checkAttributesTest
      : (IrNodePath, Boolean, IrNodeMatchingRules) => (IrNodeAttributes, IrNodeAttributes) => IrNodeEqualityResult =
    (path, isXml, rules) =>
      (a, b) =>
        foldResults(a.attributes.toList.map { p =>
          b.attributes.get(p._1) match {
            case None =>
              IrNodesNotEqual(s"Attribute ${p._1} was missing", path)

            case Some(v: IrNodeAttribute) =>
              if (v == p._2) IrNodesEqual
              else {
                RuleChecks
                  .checkForPrimitive(
                    rules,
                    p._2.path,
                    Option(p._2.value),
                    Option(v.value),
                    checkParentTypeRule = true,
                    isXml
                  )
                  .getOrElse(
                    IrNodesNotEqual(
                      s"Attribute value for '${p._1}' of '${p._2.value.renderAsString}' does not equal '${v.value.renderAsString}'",
                      path
                    )
                  )
              }
          }
        })

  val attributesTest: (Boolean, Boolean, Boolean, IrNodePath, IrNodeMatchingRules) => (
      IrNodeAttributes,
      IrNodeAttributes
  ) => IrNodeEqualityResult =
    (strict, isXml, bePermissive, path, rules) =>
      (a, b) =>
        if (strict) {
          val as      = a.attributes.toList
          val bs      = b.attributes.toList
          val asNames = as.map(_._1)
          val bsNames = bs.map(_._1)

          if (asNames.length != bsNames.length && !bePermissive) {
            IrNodesNotEqual(
              s"Differing number of attributes between ['${asNames.mkString(", ")}'] and ['${bsNames.mkString(", ")}']",
              path
            )
          } else {
            checkAttributesTest(path, isXml, rules)(a, b)
          }

        } else checkAttributesTest(path, isXml, rules)(a, b)

  def check[A](f: (A, A) => IrNodeEqualityResult, propA: A, propB: A): IrNodeEqualityResult = f(propA, propB)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy