summaryrefslogtreecommitdiff
path: root/src/main/scala/cc/spray/json/JsonParser.scala
diff options
context:
space:
mode:
authorMathias <mathias@spray.cc>2011-05-06 11:11:37 +0200
committerMathias <mathias@spray.cc>2011-05-06 11:11:37 +0200
commit0ce9cf8fce1dc475f3bb2a517e0a7698c9e0a5d9 (patch)
tree62de608642a30ec478411d27e94179ec64c5bd17 /src/main/scala/cc/spray/json/JsonParser.scala
downloadspray-json-0ce9cf8fce1dc475f3bb2a517e0a7698c9e0a5d9.tar.gz
spray-json-0ce9cf8fce1dc475f3bb2a517e0a7698c9e0a5d9.tar.bz2
spray-json-0ce9cf8fce1dc475f3bb2a517e0a7698c9e0a5d9.zip
Initial commit (split off from main spray codebase)
Diffstat (limited to 'src/main/scala/cc/spray/json/JsonParser.scala')
-rw-r--r--src/main/scala/cc/spray/json/JsonParser.scala121
1 files changed, 121 insertions, 0 deletions
diff --git a/src/main/scala/cc/spray/json/JsonParser.scala b/src/main/scala/cc/spray/json/JsonParser.scala
new file mode 100644
index 0000000..275f310
--- /dev/null
+++ b/src/main/scala/cc/spray/json/JsonParser.scala
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2009-2011 Mathias Doenitz
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package cc.spray.json
+
+import org.parboiled.scala._
+import org.parboiled.errors.{ErrorUtils, ParsingException}
+import java.lang.StringBuilder
+import org.parboiled.Context
+
+/**
+ * This JSON parser is the almost direct implementation of the JSON grammar
+ * presented at http://www.json.org as a parboiled PEG parser.
+ */
+object JsonParser extends Parser {
+
+ // the root rule
+ def Json = rule { WhiteSpace ~ Value ~ EOI }
+
+ def JsonObject: Rule1[JsObject] = rule {
+ "{ " ~ zeroOrMore(Pair, separator = ", ") ~ "} " ~~> (JsObject(_))
+ }
+
+ def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value ~~> (JsField(_, _)) }
+
+ def Value: Rule1[JsValue] = rule {
+ JsonString | JsonNumber | JsonObject | JsonArray | JsonTrue | JsonFalse | JsonNull
+ }
+
+ def JsonString = rule { JsonStringUnwrapped ~~> JsString }
+
+ def JsonStringUnwrapped = rule { "\"" ~ Characters ~ "\" " ~~> (_.toString) }
+
+ def JsonNumber = rule { group(Integer ~ optional(Frac) ~ optional(Exp)) ~> (JsNumber(_)) ~ WhiteSpace }
+
+ def JsonArray = rule { "[ " ~ zeroOrMore(Value, separator = ", ") ~ "] " ~~> (JsArray(_)) }
+
+ def Characters = rule { push(new StringBuilder) ~ zeroOrMore("\\" ~ EscapedChar | NormalChar) }
+
+ def EscapedChar = rule (
+ anyOf("\"\\/") ~:% withContext(appendToSb(_)(_))
+ | "b" ~ appendToSb('\b')
+ | "f" ~ appendToSb('\f')
+ | "n" ~ appendToSb('\n')
+ | "r" ~ appendToSb('\r')
+ | "t" ~ appendToSb('\t')
+ | Unicode ~~% withContext((code, ctx) => appendToSb(code.asInstanceOf[Char])(ctx))
+ )
+
+ def NormalChar = rule { !anyOf("\"\\") ~ ANY ~:% (withContext(appendToSb(_)(_))) }
+
+ def Unicode = rule { "u" ~ group(HexDigit ~ HexDigit ~ HexDigit ~ HexDigit) ~> (java.lang.Integer.parseInt(_, 16)) }
+
+ def Integer = rule { optional("-") ~ (("1" - "9") ~ Digits | Digit) }
+
+ def Digits = rule { oneOrMore(Digit) }
+
+ def Digit = rule { "0" - "9" }
+
+ def HexDigit = rule { "0" - "9" | "a" - "f" | "A" - "Z" }
+
+ def Frac = rule { "." ~ Digits }
+
+ def Exp = rule { ignoreCase("e") ~ optional(anyOf("+-")) ~ Digits }
+
+ def JsonTrue = rule { "true " ~ push(JsTrue) }
+
+ def JsonFalse = rule { "false " ~ push(JsFalse) }
+
+ def JsonNull = rule { "null " ~ push(JsNull) }
+
+ def WhiteSpace: Rule0 = rule { zeroOrMore(anyOf(" \n\r\t\f")) }
+
+ // helper method for fast string building
+ // for maximum performance we use a somewhat unorthodox parsing technqiue that is a bit more verbose (and probably
+ // less readable) but reduces object allocations during the parsing run to a minimum:
+ // the Characters rules pushes a StringBuilder object onto the stack which is then directly fed with matched
+ // and unescaped characters in the sub rules (i.e. no string allocations and value stack operation required)
+ def appendToSb(c: Char): Context[Any] => Unit = { ctx =>
+ ctx.getValueStack.peek.asInstanceOf[StringBuilder].append(c)
+ ()
+ }
+
+ /**
+ * We redefine the default string-to-rule conversion to also match trailing whitespace if the string ends with
+ * a blank, this keeps the rules free from most whitespace matching clutter
+ */
+ override implicit def toRule(string: String) = {
+ if (string.endsWith(" ")) str(string.trim) ~ WhiteSpace
+ else str(string)
+ }
+
+ /**
+ * The main parsing method. Uses a ReportingParseRunner (which only reports the first error) for simplicity.
+ */
+ def apply(json: String): JsValue = apply(json.toCharArray)
+
+ /**
+ * The main parsing method. Uses a ReportingParseRunner (which only reports the first error) for simplicity.
+ */
+ def apply(json: Array[Char]): JsValue = {
+ val parsingResult = ReportingParseRunner(Json).run(json)
+ parsingResult.result.getOrElse {
+ throw new ParsingException("Invalid JSON source:\n" + ErrorUtils.printParseErrors(parsingResult))
+ }
+ }
+
+} \ No newline at end of file