diff options
author | Johannes Rudolph <johannes_rudolph@gmx.de> | 2012-05-31 15:50:25 +0200 |
---|---|---|
committer | Johannes Rudolph <johannes_rudolph@gmx.de> | 2012-05-31 15:50:25 +0200 |
commit | 816cc37ceed087445116fad010485ab139975be0 (patch) | |
tree | 95294bc09f1e3663dfe0f8db64a22cc61203e6e1 /src/main/scala/cc | |
parent | e5555bf2da861ce365b21d27309daf4d2de3580f (diff) | |
parent | 216c7cca194986410e845b0d2d9eefdd58b9fa3d (diff) | |
download | spray-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.scala | 131 | ||||
-rw-r--r-- | src/main/scala/cc/spray/json/lenses/JsonLenses.scala | 48 |
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 |