diff options
author | Mathias <mathias@spray.cc> | 2011-05-06 11:11:37 +0200 |
---|---|---|
committer | Mathias <mathias@spray.cc> | 2011-05-06 11:11:37 +0200 |
commit | 0ce9cf8fce1dc475f3bb2a517e0a7698c9e0a5d9 (patch) | |
tree | 62de608642a30ec478411d27e94179ec64c5bd17 /src/main/scala/cc/spray/json/JsonParser.scala | |
download | spray-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.scala | 121 |
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 |