summaryrefslogtreecommitdiff
path: root/src/main/scala/cc
diff options
context:
space:
mode:
authorJohannes Rudolph <johannes_rudolph@gmx.de>2012-05-31 15:50:25 +0200
committerJohannes Rudolph <johannes_rudolph@gmx.de>2012-05-31 15:50:25 +0200
commit816cc37ceed087445116fad010485ab139975be0 (patch)
tree95294bc09f1e3663dfe0f8db64a22cc61203e6e1 /src/main/scala/cc
parente5555bf2da861ce365b21d27309daf4d2de3580f (diff)
parent216c7cca194986410e845b0d2d9eefdd58b9fa3d (diff)
downloadspray-json-816cc37ceed087445116fad010485ab139975be0.tar.gz
spray-json-816cc37ceed087445116fad010485ab139975be0.tar.bz2
spray-json-816cc37ceed087445116fad010485ab139975be0.zip
Merge branch 'json-path-support'
Diffstat (limited to 'src/main/scala/cc')
-rw-r--r--src/main/scala/cc/spray/json/JsonPathParser.scala131
-rw-r--r--src/main/scala/cc/spray/json/lenses/JsonLenses.scala48
2 files changed, 179 insertions, 0 deletions
diff --git a/src/main/scala/cc/spray/json/JsonPathParser.scala b/src/main/scala/cc/spray/json/JsonPathParser.scala
new file mode 100644
index 0000000..ecf2dbb
--- /dev/null
+++ b/src/main/scala/cc/spray/json/JsonPathParser.scala
@@ -0,0 +1,131 @@
+package cc.spray.json
+
+import org.parboiled.scala._
+import org.parboiled.errors.{ErrorUtils, ParsingException}
+
+object JsonPathParser extends Parser {
+ def JsonPathExpr = rule { Path ~ EOI }
+
+ def Path: Rule1[JsonPath.Path] = rule { Root ~ OptionalSelection }
+
+ def Root: Rule1[JsonPath.Root.type] = rule {
+ // we don't distinguish between '$' and '@'
+ anyOf("$@") ~ push(JsonPath.Root)
+ }
+
+ /*
+ * To remove the left-recursion I had to factor out Root to here
+ */
+ def OptionalSelection : ReductionRule1[JsonPath.Path, JsonPath.Path] = rule {
+ Projection ~~> JsonPath.Selection ~ OptionalSelection |
+ EMPTY ~~> identity
+ }
+
+ def Projection: Rule1[JsonPath.Projection] = rule {
+ "." ~ DotProjection |
+ "[" ~ BracketProjection ~"]"
+ }
+
+ def DotProjection: Rule1[JsonPath.Projection] = rule {
+ ByFieldName
+ }
+ def AllElements = rule { "*" ~ push(JsonPath.AllElements) }
+ def ByFieldName = rule { FieldName ~~> JsonPath.ByField }
+
+ import JsonParser.WhiteSpace
+ def BracketProjection: Rule1[JsonPath.Projection] = rule {
+ JsonParser.Digits ~> (d => JsonPath.ByIndex(d.toInt)) |
+ SingleQuotedString ~~> JsonPath.ByField |
+ AllElements |
+ "?(" ~ WhiteSpace ~ Predicate ~ WhiteSpace ~ ")" ~~> JsonPath.ByPredicate
+ }
+
+ def Predicate: Rule1[JsonPath.Predicate] = rule {
+ Lt | Gt | Eq | Exists
+ }
+ def Eq: Rule1[JsonPath.Eq] = rule { op("==")(JsonPath.Eq) }
+ def Lt: Rule1[JsonPath.Lt] = rule { op("<")(JsonPath.Lt) }
+ def Gt: Rule1[JsonPath.Gt] = rule { op(">")(JsonPath.Gt) }
+ def Exists: Rule1[JsonPath.Exists] = rule {
+ Path ~~> JsonPath.Exists
+ }
+
+ def op[T](op: String)(cons: (JsonPath.Expr, JsonPath.SimpleExpr) => T) =
+ Expr ~ WhiteSpace ~ op ~ WhiteSpace ~ SimpleExpr ~~> cons
+
+ def Expr: Rule1[JsonPath.Expr] = rule {
+ Path ~~> JsonPath.PathExpr |
+ SimpleExpr
+ }
+ def SimpleExpr: Rule1[JsonPath.SimpleExpr] = rule {
+ JsConstant ~~> JsonPath.Constant
+ }
+ def JsConstant: Rule1[JsValue] = rule {
+ JsonParser.JsonNumber |
+ SingleQuotedString ~~> (JsString(_))
+ }
+
+ val WhiteSpaceChars = " \n\r\t\f"
+ def FieldName: Rule1[String] = rule {
+ oneOrMore(!anyOf(".[)]"+WhiteSpaceChars) ~ ANY) ~> identity
+ }
+
+ def SingleQuotedString: Rule1[String] =
+ rule { "'" ~ push(new java.lang.StringBuilder) ~ zeroOrMore(!anyOf("'") ~ ("\\" ~ JsonParser.EscapedChar | JsonParser.NormalChar)) } ~ "'" ~~> (_.toString)
+
+ /**
+ * The main parsing method. Uses a ReportingParseRunner (which only reports the first error) for simplicity.
+ */
+ def apply(path: String): JsonPath.Path = apply(path.toCharArray)
+
+ /**
+ * The main parsing method. Uses a ReportingParseRunner (which only reports the first error) for simplicity.
+ */
+ def apply(path: Array[Char]): JsonPath.Path = {
+ val parsingResult = ReportingParseRunner(JsonPathExpr).run(path)
+ parsingResult.result.getOrElse {
+ throw new ParsingException("Invalid JSON source:\n" + ErrorUtils.printParseErrors(parsingResult))
+ }
+ }
+}
+
+object JsonPath {
+ sealed trait Path
+ case object Root extends Path
+ case class Selection(previous: Path, projection: Projection) extends Path
+
+ sealed trait Projection
+ case object AllElements extends Projection
+ case class ByField(name: String) extends Projection
+ case class ByIndex(idx: Int) extends Projection
+ case class ByPredicate(expr: Predicate) extends Projection
+
+ sealed trait Predicate
+ sealed trait BinOpPredicate extends Predicate {
+ def expr1: Expr
+ def expr2: SimpleExpr
+
+ def predicate(v1: JsValue, v2: JsValue): Boolean
+ }
+ case class Eq(expr1: Expr, expr2: SimpleExpr) extends BinOpPredicate {
+ def predicate(v1: JsValue, v2: JsValue): Boolean = v1 == v2
+ }
+ case class Lt(expr1: Expr, expr2: SimpleExpr) extends BinOpPredicate {
+ def predicate(v1: JsValue, v2: JsValue): Boolean = (v1, v2) match {
+ case (JsNumber(n1), JsNumber(n2)) => n1 < n2
+ case _ => false
+ }
+ }
+ case class Gt(expr1: Expr, expr2: SimpleExpr) extends BinOpPredicate {
+ def predicate(v1: JsValue, v2: JsValue): Boolean = (v1, v2) match {
+ case (JsNumber(n1), JsNumber(n2)) => n1 > n2
+ case _ => false
+ }
+ }
+ case class Exists(path: Path) extends Predicate
+
+ sealed trait Expr
+ sealed trait SimpleExpr extends Expr
+ case class PathExpr(path: Path) extends Expr
+ case class Constant(value: JsValue) extends SimpleExpr
+} \ No newline at end of file
diff --git a/src/main/scala/cc/spray/json/lenses/JsonLenses.scala b/src/main/scala/cc/spray/json/lenses/JsonLenses.scala
index 8871f9d..1468565 100644
--- a/src/main/scala/cc/spray/json/lenses/JsonLenses.scala
+++ b/src/main/scala/cc/spray/json/lenses/JsonLenses.scala
@@ -148,6 +148,8 @@ object JsonLenses {
trait Projection[M[_]] extends UpdateLens with ReadLens[M] {
def /[M2[_], R[_]](next: Projection[M2])(implicit ev: Join[M2, M, R]): Projection[R]
+ def toSeq: Projection[Seq]
+
def ops: Ops[M]
}
@@ -262,6 +264,8 @@ object JsonLenses {
outer.updated(_.flatMap(next.updated(f)))(parent)
}
+ def toSeq: Projection[Seq] = this / asSeq
+
private[this] def mapValue[T](value: M[JsValue])(f: JsValue => Validated[T]): Validated[M[T]] =
ops.allRight(ops.map(value)(f))
}
@@ -322,6 +326,12 @@ object JsonLenses {
def retr: JsValue => SafeJsValue = x => Right(x)
}
+ val asSeq: SeqProjection = new Proj[Seq] {
+ def updated(f: Operation)(parent: JsValue): SafeJsValue =
+ f(Right(parent))
+
+ def retr: JsValue => JsonLenses.Validated[Seq[JsValue]] = x => Right(Seq(x))
+ }
val elements: SeqProjection = new Proj[Seq] {
def updated(f: SafeJsValue => SafeJsValue)(parent: JsValue): SafeJsValue = parent match {
@@ -402,4 +412,42 @@ object JsonLenses {
def unexpected(message: String) = Left(new RuntimeException(message))
def outOfBounds(message: String) = Left(new IndexOutOfBoundsException(message))
+
+ def fromPath(path: String): Projection[Seq] = {
+ val ast = JsonPathParser(path)
+
+ def convertPath(path: JsonPath.Path): Projection[Seq] = path match {
+ case JsonPath.Root => value.toSeq
+ case JsonPath.Selection(inner, proj) => convertPath(inner) / convertProjection(proj)
+ }
+ def convertProjection(proj: JsonPath.Projection): Projection[Seq] =
+ proj match {
+ case JsonPath.ByField(name) => field(name).toSeq
+ case JsonPath.ByIndex(i) => element(i).toSeq
+ case JsonPath.AllElements => elements
+ case JsonPath.ByPredicate(pred) => filter(convertPredicate(pred))
+ }
+ def convertPredicate(pred: JsonPath.Predicate): JsPred = pred match {
+ case op: JsonPath.BinOpPredicate =>
+ val f1 = convertExpr(op.expr1)
+ val f2 = convertSimpleExpr(op.expr2)
+
+ js => {
+ val v2 = f2(js)
+ f1(js).right.forall(_.forall(v1 => op.predicate(v1, v2)))
+ }
+
+ case JsonPath.Exists(path) =>
+ js => convertPath(path).retr(js).isRight
+ }
+ def convertExpr(expr: JsonPath.Expr): JsValue => Validated[Seq[JsValue]] = expr match {
+ case JsonPath.PathExpr(path) => js => convertPath(path).retr(js)
+ case simple: JsonPath.SimpleExpr => js => Right(Seq(convertSimpleExpr(simple)(js)))
+ }
+ def convertSimpleExpr(expr: JsonPath.SimpleExpr): JsValue => JsValue = expr match {
+ case JsonPath.Constant(x) => _ => x
+ }
+
+ convertPath(ast)
+ }
} \ No newline at end of file