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

com.rojoma.json.v3.matcher.Matcher.scala Maven / Gradle / Ivy

The newest version!
package com.rojoma.json.v3
package matcher

import scala.language.implicitConversions

import ast._
import codec._

import `-impl`.matcher._

// Note: in a couple of places in this file are type parameters with bounds
// like ": JsonDecode : JsonEncode"; this is the opposite order to what is
// conventional, but cannot be changed for binary compatibility reasons.

class JsonGenerationException extends RuntimeException("Cannot generate JSON; this is always a logic error.  You've forgotten to bind a variable, or you've used a pattern that cannot generate.")

/** Either a [[com.rojoma.json.v3.matcher.Pattern]] or a [[com.rojoma.json.v3.matcher.POption]]. */
sealed trait OptPattern

object OptPattern extends LowPriorityImplicits {
  implicit def litifyJValue(x: JValue): Pattern = x match {
    case atom: JAtom => Literal(atom)
    case JArray(arr) => PArray(arr.map(litifyJValue) : _*)
    case JObject(obj) => PObject(obj.mapValues(litifyJValue).toSeq : _*)
  }

  /** Converts an object with a [[com.rojoma.json.v3.codec.JsonDecode]]
   * and [[com.rojoma.json.v3.codec.JsonEncode]] into a
   * [[com.rojoma.json.v3.matcher.Pattern]] which matches a value only
   * if the codec can decode it into something which is `equal` to the
   * object. */
  implicit def litifyCodec[T : JsonDecode : JsonEncode](lit: T): Pattern =
    new Pattern {
      def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] =
        JsonDecode[T].decode(x) match {
          case Right(y) if lit == y => Right(environment)
          case Right(_) => Left(DecodeError.InvalidValue(x))
          case Left(err) => Left(err)
        }

      def generate(environment: Pattern.Results): Option[JValue] = Some(JsonEncode[T].encode(lit))
    }
}

/** An object that can be used to either match and extract data from,
 * or generate, [[com.rojoma.json.v3.ast.JValue]]s. */
trait Pattern extends OptPattern {
  /** Tests the given [[com.rojoma.json.v3.ast.JValue]] against this `Pattern`,
   * and if it matches returns an object that can be used to retrieve the
   * values matched by any [[com.rojoma.json.v3.matcher.Variable]]s in the
   * `Pattern`.
   *
   * @example {{{
   * val intVar = Variable[Int]
   * val strVar = Variable[String]
   * val pattern = PObject("i" -> intVar, "s" -> strVar)
   *
   * pattern.matches(jvalue) match {
   *   case Right(results) => println("The integer was " + intVar(results))
   *   case Left(_) => println("It didn't match the pattern")
   * }
   * }}}
   *
   * @param x The value to test.
   * @return An environment which can be used to look up variable bindings, or `Left(error)` if it didn't match.
   */
  def matches(x: JValue) = evaluate(x, Map.empty)

  /** Tests the given [[com.rojoma.json.v3.ast.JValue]] against this `Pattern`, with the
   * restriction that any [[com.rojoma.json.v3.matcher.Variable]]s that are bound in the
   * `environment` must match those values if they are re-used in this `Pattern`.
   *
   * Generally you won't use this directly, but you can if you want to pre-populate variables.
   *
   * @param x The value to test.
   * @return The environment augmented with any new [[com.rojoma.json.v3.matcher.Variable]]s
   *    encountered in this `Pattern`, or `Left(error)` if it didn't match. */
  def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results]

  /** Uses this `Pattern` together with the provided variable bindings to generate a
   * new [[com.rojoma.json.v3.ast.JValue]].
   *
   * @example {{{
   * val intVar = Variable[Int]
   * val strVar = Variable[String]
   * val pattern = PObject("i" -> intVar, "s" -> POption(strVar))
   *
   * pattern.generate(i := 1)                      // { "i" : 1 }
   * pattern.generate(i := 1, s := "hello")        // { "i" : 1, "s" : "hello" }
   * pattern.generate(i := 1, s :=? None )         // { "i" : 1 }
   * pattern.generate(i := 1, s :=? Some("hello")) // { "i" : 1, "s" : "hello" }
   * }}}
   *
   * @return The new [[com.rojoma.json.v3.ast.JValue]]
   * @throws JsonGenerationException if a required [[com.rojoma.json.v3.matcher.Variable]]
   *   is not bound or a matcher which cannot generate (such as [[com.rojoma.json.v3.matcher.AllOf]])
   *   is used.
   */
  def generate(bindings: (Pattern.Results => Pattern.Results)*): JValue =
    generate(bindings.foldLeft(Map.empty : Pattern.Results) { (e, b) => b(e) }).getOrElse(throw new JsonGenerationException)

  /** Uses this `Pattern` together with the bindings generated as the result of a
   * call to `matches` or `evaluate` to produce a [[com.rojoma.json.v3.ast.JValue]].
   *
   * Generally the other `generate` method is simpler to use.
   *
   * @return The new [[com.rojoma.json.v3.ast.JValue]], or None if a required
   *   [[com.rojoma.json.v3.matcher.Variable]] is not bound in the environment,
   *   or a matcher which cannot generate is used.
   */
  def generate(environment: Pattern.Results): Option[JValue]

  /** Allows this `Pattern` to be used in a `match` expression, with the output
   * being the environment of [[com.rojoma.json.v3.matcher.Variable]] bindings
   * as produced by `matches`.
   *
   * @example {{{
   * val intVar = Variable[Int]
   * val strVar = Variable[String]
   * val Pattern1 = PObject("i" -> intVar, "s" -> strVar)
   * val Pattern2 = PObject("hello" -> "world")
   *
   * jvalue match {
   *   case Pattern1(results) => println("The integer was " + intVar(results))
   *   case Pattern2(result) => println("It was just a hello world object")
   *   case _ => println("It was something else")
   * }
   * }}}
   */
  def unapply(x: JValue): Option[Pattern.Results] = matches(x).right.toOption
}

object Pattern {
  type Results = Map[Variable[_], Any]

  private[matcher] def foldMatches[A, B](seq: Iterable[B], init: A)(f: (A, B, Int) => Either[DecodeError, A]): Either[DecodeError, A] = {
    val it = seq.iterator
    var i = 0
    var acc = init
    while(it.hasNext) {
      f(acc, it.next(), i) match {
        case Right(r) => acc = r
        case err@Left(_) => return err
      }
      i += 1
    }
    return Right(acc)
  }

  private[matcher] def foldLeftOpt[A, B](seq: Iterable[B], init: A)(f: (A, B) => Option[A]): Option[A] = {
    val it = seq.iterator
    var acc = init
    while(it.hasNext) {
      f(acc, it.next()) match {
        case Some(r) => acc = r
        case None => return None
      }
    }
    return Some(acc)
  }
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches a [[com.rojoma.json.v3.ast.JValue]]
 * exactly.  Generally this is not used explicitly; [[com.rojoma.json.v3.ast.JValue]]s
 * can be implicitly converted into `Literal`s. */
case class Literal(literal: JValue) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] =
    if(x == literal) Right(environment)
    else if(x.jsonType == literal.jsonType) Left(DecodeError.InvalidValue(x))
    else Left(DecodeError.InvalidType(literal.jsonType, x.jsonType))

  def generate(environment: Pattern.Results) = Some(literal)
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches a [[com.rojoma.json.v3.ast.JValue]]
 * if a predicate on that JValue is true.  This `Pattern` cannot be used to generate a
 * [[com.rojoma.json.v3.ast.JValue]]. */
case class FLiteral(recognizer: JValue => Boolean) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] =
    if(recognizer(x)) Right(environment)
    else Left(DecodeError.InvalidValue(x))

  def generate(environment: Pattern.Results): Option[JValue] = None
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches any [[com.rojoma.json.v3.ast.JValue]]
 * which can be decoded into an object of type `T`.  If this variable is
 * already bound in the environment at match-time, then this matches only if
 * the two decoded objects are `equal`, in which case the environment is
 * unchanged. */
abstract class Variable[T] extends Pattern with PartialFunction[Pattern.Results, T] {
  /** Look up the value of this variable in an environment.
   *
   * @return The value found
   * @throws NoSuchElementException if the variable is not bound. */
  def apply(results: Pattern.Results): T =
    results(this).asInstanceOf[T]

  /** Look up the value of this variable in an environment.
   *
   * @return The value found, or None if it was not bound. */
  def get(results: Pattern.Results): Option[T] =
    results.get(this).map(_.asInstanceOf[T])

  /** Look up the value of this variable in an environment.
   *
   * @return The value found, or `alternative` if it was not bound. */
  def getOrElse[U >: T](results: Pattern.Results, alternative: => U): U =
    results.get(this).map(_.asInstanceOf[T]).getOrElse(alternative)

  def isDefinedAt(results: Pattern.Results) = results.isDefinedAt(this)

  def isBound(results: Pattern.Results) = isDefinedAt(results)

  /** Bind this variable into an environment.  This is usually used with
   * [[com.rojoma.json.v3.matcher.Pattern]]#generate.
   *
   * @example {{{
   * val intVar = Variable[Int]
   * val pattern = PObject("i" -> intVar)
   *
   * println(pattern.generate(intVar := 5)) // { "i" : 5 }
   * }}} */
  def := (x: T): Pattern.Results => Pattern.Results = _ + (this -> x)

  /** Possibly this variable into an environment.  This is usually used with
   * [[com.rojoma.json.v3.matcher.Pattern]]#generate.
   *
   * @example {{{
   * val intVar = Variable[Int]
   * val pattern = PObject("i" -> POption(intVar))
   *
   * println(pattern.generate(intVar :=? Some(5))) // { "i" : 5 }
   * println(pattern.generate(intVar :=? None)) // { }
   * }}} */
  def :=? (x: Option[T]): Pattern.Results => Pattern.Results = x match {
    case Some(x) => this := x
    case None => identity
  }
}

object Variable {
  private abstract class DecodingVariable[T](dec: JsonDecode[T]) extends Variable[T] {
    def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] = {
      dec.decode(x) match {
        case Right(r1) =>
          environment.get(this) match {
            case None =>
              Right(environment + (this -> r1))
            case Some(r2) if r2 == r1 =>
              Right(environment)
            case _ =>
              Left(DecodeError.InvalidValue(x))
          }
        case Left(err) =>
          Left(err)
      }
    }
  }

  def apply[T : JsonDecode : JsonEncode](): Variable[T] =
    new DecodingVariable(JsonDecode[T]) {
      def generate(environment: Pattern.Results) =
        get(environment).map(JsonEncode[T].encode)
    }

  // I'd like to have an explicit
  //   decodeOnly[T](decode: JsonDecode[T])
  // too, but it erases to the same as the implicit version.
  // Since an extra pair of parens is not a huge deal, I'll
  // just let decodeOnly get called with an explicit evidence
  // parameter if it matters.  In practice, I've found decode-
  // only patterns where you in fact don't have an encoder
  // aren't used all THAT much.

  def decodeOnly[T : JsonDecode](): Variable[T] =
    new DecodingVariable(JsonDecode[T]) {
      def generate(environment: Pattern.Results) =
        None
    }

  def apply[T](codec: JsonEncode[T] with JsonDecode[T]): Variable[T] = apply()(codec, codec)
  def apply[T](encode: JsonEncode[T], decode: JsonDecode[T]): Variable[T] = apply()(decode, encode)
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches if the value is
 * a [[com.rojoma.json.v3.ast.JArray]] which contains at least as many elements
 * as sub-patterns contained by this object and those elements match the
 * sub-patterns in the order given.
 *
 * For the more common case where a sequence of repeated objects of a
 * particular type is desired, use a [[com.rojoma.json.v3.matcher.Variable]]
 * of some `Seq[T]`.
 */
case class PArray(subPatterns: Pattern*) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] =
    x match {
      case arr: JArray =>
        if(arr.length != subPatterns.length) {
          Left(DecodeError.InvalidLength(expected = subPatterns.length, got = arr.length))
        } else {
          Pattern.foldMatches(arr zip subPatterns, environment) { (env, vp, i) =>
            val (subValue, subPattern) = vp
            subPattern.evaluate(subValue, env) match {
              case r@Right(_) => r
              case Left(err) => return Left(err.prefix(i))
            }
          }
        }
      case other =>
        Left(DecodeError.InvalidType(JArray, other.jsonType))
    }

  def generate(environment: Pattern.Results) = {
    val subValues = subPatterns.map(_.generate(environment))
    if(subValues.forall(_.isDefined))
      Some(JArray(subValues.map(_.get)))
    else
      None
  }
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches if the value is
 * a [[com.rojoma.json.v3.ast.JObject]] which contains at least the fields
 * specified in this `Pattern`.  In order to allow fields to not appear
 * at all, the sub-patterns can be wrapped in a [[com.rojoma.json.v3.matcher.POption]].
 *
 * @example {{{
 *   val i = Variable[Int]
 *   val s = Variable[String]
 *   val b = Variable[Boolean]
 *   val f = Variable[Float]
 *   val pattern = PObject(
 *     "i" -> i,                 // must be present
 *     "s" -> POption(s),        // may be absent but must not be null
 *     "b" -> POption(b).orNull, // may be present, absent, or null
 *     "f" -> FirstOf(f, JNull)  // must be present but may be null
 *   )
 *
 *   pattern.matches(jvalue) match {
 *     case Some(results) =>
 *       println("i: " + i(results))
 *       println("s: " + s.getOrElse(results, "[not present]"))
 *       println("b: " + b.getOrElse(results, "[not present or null]"))
 *       println("f: " + f.getOrElse(results, "[null]"))
 *    case None =>
 *       println("Didn't match")
 *   }
 * }}}
 */
case class PObject(subPatterns: (String, OptPattern)*) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results) = x match {
    case obj: JObject =>
      Pattern.foldMatches(subPatterns, environment) { (env, sp, _) =>
        sp match {
          case (subKey, subPat: Pattern) =>
            obj.get(subKey) match {
              case Some(subValue) =>
                subPat.evaluate(subValue, env) match {
                  case r@Right(_) => r
                  case Left(err) => Left(err.prefix(subKey))
                }
              case None =>
                Left(DecodeError.MissingField(subKey))
            }
          case (subKey, POption(subPat)) =>
            obj.get(subKey) match {
              case Some(subValue) =>
                subPat.evaluate(subValue, env) match {
                  case r@Right(_) => r
                  case Left(err) => Left(err.prefix(subKey))
                }
              case None =>
                Right(env)
            }
        }
      }
    case other =>
      Left(DecodeError.InvalidType(JObject, other.jsonType))
  }


  def generate(environment: Pattern.Results) = {
    val newObject = Pattern.foldLeftOpt(subPatterns, Map.empty[String, JValue]) { (result, sp) =>
      def require(subKey: String, subPat: Pattern) = {
        subPat.generate(environment) match {
          case Some(subValue) =>
            Some(result + (subKey -> subValue))
          case None =>
            None
        }
      }
      def permit(subKey: String, subPat: Pattern) = {
        subPat.generate(environment) match {
          case Some(subValue) =>
            Some(result + (subKey -> subValue))
          case None =>
            Some(result)
        }
      }
      sp match {
        case (subKey, subPat: Pattern) => require(subKey, subPat)
        case (subKey, POption(FirstOf(subPat, Literal(JNull)))) => permit(subKey, subPat)
        case (subKey, POption(subPat)) => permit(subKey, subPat)
      }
    }
    newObject.map(JObject)
  }
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches the first
 * sub-pattern to succeed.  This is frequently used to allow a
 * value to be something-or-`null`. */
case class FirstOf(subPatterns: Pattern*) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results) = {
    val it = subPatterns.iterator
    def loop(fails: List[DecodeError]): Either[DecodeError, Pattern.Results] = {
      if(!it.hasNext) Left(DecodeError.join(fails.reverse))
      else it.next().evaluate(x, environment) match {
        case Right(res) => Right(res)
        case Left(err) => loop(err :: fails)
      }
    }
    loop(Nil)
  }

  def generate(environment: Pattern.Results) = {
    val it = subPatterns.iterator
    def loop(): Option[JValue] = {
      if(!it.hasNext) None
      else it.next().generate(environment) match {
        case None => loop()
        case res => res
      }
    }
    loop()
  }
}

/** A [[com.rojoma.json.v3.matcher.Pattern]] which matches only if
 * all the sub-patterns also match.  This pattern cannot be
 * used to `generate` a [[com.rojoma.json.v3.ast.JValue]]. */
case class AllOf(subPatterns: OptPattern*) extends Pattern {
  def evaluate(x: JValue, environment: Pattern.Results): Either[DecodeError, Pattern.Results] =
    Pattern.foldMatches(subPatterns, environment) { (env, subPat, _) =>
      subPat match {
        case pat: Pattern =>
          pat.evaluate(x, env) // No need to augment
        case POption(pat) =>
          pat.evaluate(x, env) match {
            case Right(auged) => Right(auged)
            case Left(_) => Right(env)
          }
      }
    }

  def generate(environment: Pattern.Results) = None
}

/** A wrapper for a [[com.rojoma.json.v3.matcher.Pattern]] which allows
 * fields to be absent when using a [[com.rojoma.json.v3.matcher.PObject]].
 *
 * @see [[com.rojoma.json.v3.matcher.PObject]] */
case class POption(subPattern: Pattern) extends OptPattern {
  /** Shorthand to allow a value to be present, absent, or `null`, with the
   * absent and null cases considered equivalent.  During generation,
   * this will produce no field at all if the subpattern cannot generate a
   * value. */
  def orNull = POption(FirstOf(subPattern, Literal(JNull)))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy