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

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

package com.itv.scalapact.shared.matchir

import com.itv.scalapact.shared.matchir.IrNodePath.IrNodePathEmpty
import com.itv.scalapact.shared.matchir.PactPathParseResult.{PactPathParseFailure, PactPathParseSuccess}

import scala.annotation.tailrec
import scala.util.matching.Regex

/** PactPath (defined in the pact standard) is JsonPath with a few tweaks to support
  * querying XML with a nearly JsonPath-like syntax. Specific modifications to JsonPath are:
  *
  * - names match to element names ($.body.animals maps to )
  * - @names match to attribute names
  * - #text match to the text elements
  *
  * JsonPath support a ["xxx"] form which we use for to escape the @ and #. e.g.
  * foo.bar["#text"]
  * foo.bar['@id']
  */
object PactPath {
  import PactPathPatterns._

  val toPactPath: IrNodePath => String = _.renderAsString

  val fromPactPath: String => PactPathParseResult = pactPath => {
    @tailrec def rec(remaining: String, acc: IrNodePath): PactPathParseResult =
      remaining match {
        // Complete, success
        case "" =>
          PactPathParseSuccess(acc)

        // Prefixes
        case dollarPrefix(_, r) =>
          rec(r, acc)

        // Fields
        case fieldNamePrefix(_, r) =>
          rec(r, acc)

        case fieldName(name, r) =>
          rec(r, acc <~ name)

        case fieldNameSuffix(_, r) =>
          rec(r, acc)

        // Arrays
        case arrayAnyElement(_, r) =>
          rec(r, acc <~ "*")

        case arrayElementAtIndex(i, r) =>
          safeStringToInt(i) match {
            case Some(index) =>
              rec(r, acc <~ index)

            case None =>
              PactPathParseFailure(pactPath, remaining, Some(s"Could not convert '$i' to array index."))
          }

        // XML Addons
        case xmlAttributeName(attributeName, r) =>
          rec(r, acc <@ attributeName)

        case xmlTextElement(_, r) =>
          rec(r, acc.text)

        // Suffixes
        case anyField(_, r) =>
          rec(r, acc.<*)

        // Unmatched, failure
        case _ =>
          PactPathParseFailure(pactPath, remaining, None)

      }

    rec(pactPath, IrNodePathEmpty)
  }

  private lazy val safeStringToInt: String => Option[Int] = str =>
    try Option(str.toInt)
    catch {
      case _: Throwable => None
    }

  object PactPathPatterns {
    // Prefixes
    val dollarPrefix: Regex = """^(\$.body|\$.headers)(.*)$""".r

    // Fields
    val fieldNamePrefix: Regex = """^(\.|\[[\'|\"])(.*)$""".r
    val fieldName: Regex =
      """^([a-zA-Z0-9:\-_]+)(.*)$""".r // TODO: This is very limited, technically any escaped string is valid
    val fieldNameSuffix: Regex = """^([\'|\"]\])(.*)$""".r

    // Arrays
    val arrayAnyElement: Regex     = """^(\[\*\])(.*)$""".r
    val arrayElementAtIndex: Regex = """^\[(\d+)\](.*)$""".r

    // Xml addons
    val xmlAttributeName: Regex = """^@([a-zA-Z0-9:\-_]+)(.*)$""".r
    val xmlTextElement: Regex   = """^#([a-zA-Z0-9:\-_]+)(.*)$""".r

    // Suffixes
    val anyField: Regex = """^(\*)(.*)$""".r
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy