├── .gitignore ├── LICENSE ├── LICENSE.scala ├── README ├── project ├── build.properties ├── build │ └── ScalaJsonProject.scala └── plugins │ └── Plugins.scala └── src ├── main └── scala │ └── com │ └── twitter │ └── json │ ├── Json.scala │ └── extensions.scala └── test └── scala └── com └── twitter └── json ├── JsonSpec.scala └── TestRunner.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | dist/ 3 | lib_managed/ 4 | project/boot/ 5 | project/plugins/project/ 6 | project/plugins/src_managed/ 7 | project/plugins/lib_managed/ 8 | project/plugins/target/ 9 | scala-json.tmproj 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.scala: -------------------------------------------------------------------------------- 1 | SCALA LICENSE 2 | 3 | Copyright (c) 2002-2010 EPFL, Lausanne, unless otherwise specified. 4 | All rights reserved. 5 | 6 | This software was developed by the Programming Methods Laboratory of the 7 | Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland. 8 | 9 | Permission to use, copy, modify, and distribute this software in source 10 | or binary form for any purpose with or without fee is hereby granted, 11 | provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in the 18 | documentation and/or other materials provided with the distribution. 19 | 20 | 3. Neither the name of the EPFL nor the names of its contributors 21 | may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 26 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 29 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 34 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 35 | SUCH DAMAGE. 36 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Scala JSON toolkit originally lifted from Martin Odersky et al's 2 | Programming Scala book. We tightened up some edge cases and added 3 | complete test coverage. 4 | 5 | Original code is under the Scala license (LICENSE.scala) and Twitter modifications 6 | are available under the Apache 2 license (LICENSE). 7 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | #Project properties 2 | #Tue Apr 26 13:30:15 PDT 2011 3 | project.organization=com.twitter 4 | project.name=json 5 | sbt.version=0.7.4 6 | project.version=2.1.7-SNAPSHOT 7 | def.scala.version=2.7.7 8 | build.scala.versions=2.8.1 2.7.7 9 | project.initialize=false 10 | -------------------------------------------------------------------------------- /project/build/ScalaJsonProject.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import com.twitter.sbt._ 3 | 4 | class ScalaJsonProject(info: ProjectInfo) extends StandardProject(info) with SubversionPublisher { 5 | val specs = buildScalaVersion match { 6 | case "2.7.7" => "org.scala-tools.testing" % "specs" % "1.6.2.1" % "test" 7 | case _ => "org.scala-tools.testing" % "specs_2.8.1" % "1.6.6" % "test" 8 | } 9 | 10 | override def subversionRepository = Some("http://svn.local.twitter.com/maven-public/") 11 | 12 | override def disableCrossPaths = false 13 | 14 | override def compileOptions = super.compileOptions ++ Seq(Unchecked) ++ 15 | compileOptions("-encoding", "utf8") 16 | 17 | override def pomExtra = 18 | 19 | 20 | Apache 2 21 | http://www.apache.org/licenses/LICENSE-2.0.txt 22 | repo 23 | 24 | 25 | Scala License 26 | http://www.scala-lang.org/node/146 27 | repo 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /project/plugins/Plugins.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | class Plugins(info: ProjectInfo) extends PluginDefinition(info) { 4 | val twitterMaven = "twitter.com" at "http://maven.twttr.com/" 5 | val defaultProject = "com.twitter" % "standard-project" % "0.7.23" 6 | } 7 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/json/Json.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.twitter.json 18 | 19 | import extensions._ 20 | import scala.util.Sorting 21 | import scala.util.parsing.combinator._ 22 | 23 | 24 | trait JsonSerializable { 25 | def toJson(): String 26 | } 27 | 28 | /** 29 | * An Exception thrown when parsing or building JSON. 30 | */ 31 | class JsonException(reason: String) extends Exception(reason) 32 | 33 | 34 | private class EscapedStringParser extends JavaTokenParsers { 35 | override protected val whiteSpace = "".r 36 | 37 | def unicode: Parser[String] = rep1("\\u" ~> """[a-fA-F0-9]{4}""".r) ^^ { stringBytes => 38 | new String(stringBytes.map(Integer.valueOf(_, 16).intValue.asInstanceOf[Char]).toArray) 39 | } 40 | 41 | def escaped: Parser[String] = "\\" ~> """[\\/bfnrt"]""".r ^^ { charStr => 42 | val char = charStr match { 43 | case "r" => '\r' 44 | case "n" => '\n' 45 | case "t" => '\t' 46 | case "b" => '\b' 47 | case "f" => '\f' 48 | case x => x.charAt(0) 49 | } 50 | char.toString 51 | } 52 | 53 | def characters: Parser[String] = """[^\"[\x00-\x1F]\\]+""".r // comment to fix emac parsing " 54 | 55 | def string: Parser[String] = "\"" ~> rep(unicode | escaped | characters) <~ "\"" ^^ { list => 56 | list.mkString("") 57 | } 58 | 59 | def parse(s: String) = { 60 | parseAll(string, s) match { 61 | case Success(result, _) => result 62 | case x @ Failure(msg, z) => throw new JsonException(x.toString) 63 | case x @ Error(msg, _) => throw new JsonException(x.toString) 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Stolen (awesomely) from the scala book and fixed by making string quotation explicit. 70 | */ 71 | private class JsonParser extends JavaTokenParsers { 72 | def obj: Parser[Map[String, Any]] = "{" ~> repsep(member, ",") <~ "}" ^^ (Map.empty ++ _) 73 | 74 | def arr: Parser[List[Any]] = "[" ~> repsep(value, ",") <~ "]" 75 | 76 | def member: Parser[(String, Any)] = string ~ ":" ~ value ^^ { 77 | case name ~ ":" ~ value => (name, value) 78 | } 79 | 80 | def number: Parser[Any] = floatingPointNumber ^^ { 81 | case num if num.matches(".*[.eE].*") => BigDecimal(num) 82 | case num => { 83 | val rv = num.toLong 84 | if (rv >= Int.MinValue && rv <= Int.MaxValue) rv.toInt else rv 85 | } 86 | } 87 | 88 | lazy val stringParser = (new EscapedStringParser) 89 | 90 | def string: Parser[String] = "\"(\\\\\\\\|\\\\\"|[^\"])*+\"".r ^^ { escapedStr => 91 | stringParser.parse(escapedStr) 92 | } 93 | 94 | def value: Parser[Any] = obj | arr | string | number | 95 | "null" ^^ (x => null) | "true" ^^ (x => true) | "false" ^^ (x => false) 96 | 97 | def parse(s: String) = { 98 | parseAll(value, s) match { 99 | case Success(result, _) => result 100 | case x @ Failure(msg, z) => throw new JsonException(x.toString) 101 | case x @ Error(msg, _) => throw new JsonException(x.toString) 102 | } 103 | } 104 | } 105 | 106 | 107 | /** 108 | * An explanation of Scala types and their JSON representations. 109 | * 110 | * Natively supported scalar types are: Boolean, Int, Long, String. 111 | * Collections are Sequence[T], Map[String, T] where T includes the scalars defined above, or 112 | * recursive Sequence or Map. You are in flavor country. 113 | */ 114 | object Json { 115 | private[json] def quotedChar(codePoint: Int) = { 116 | codePoint match { 117 | case c if c > 0xffff => 118 | val chars = Character.toChars(c) 119 | "\\u%04x\\u%04x".format(chars(0).toInt, chars(1).toInt) 120 | case c if c > 0x7e => "\\u%04x".format(c.toInt) 121 | case c => c.toChar 122 | } 123 | } 124 | 125 | /** 126 | * Quote a string according to "JSON rules". 127 | */ 128 | def quote(s: String) = { 129 | val charCount = s.codePointCount(0, s.length) 130 | "\"" + 0.to(charCount - 1).map { idx => 131 | s.codePointAt(s.offsetByCodePoints(0, idx)) match { 132 | case 0x0d => "\\r" 133 | case 0x0a => "\\n" 134 | case 0x09 => "\\t" 135 | case 0x22 => "\\\"" 136 | case 0x5c => "\\\\" 137 | case 0x2f => "\\/" // to avoid sending " quotedChar(c) 139 | } 140 | }.mkString("") + "\"" 141 | } 142 | 143 | /** 144 | * Returns a JSON representation of the given object, as a JsonQuoted object. 145 | */ 146 | def build(obj: Any): JsonQuoted = { 147 | val rv = obj match { 148 | case JsonQuoted(body) => body 149 | case null => "null" 150 | case x: Boolean => x.toString 151 | case x: Number => x.toString 152 | case array: Array[_] => array.map(build(_).body).mkString("[", ",", "]") 153 | case list: Seq[_] => 154 | list.map(build(_).body).mkString("[", ",", "]") 155 | case map: scala.collection.Map[_, _] => 156 | Sorting.stableSort[(Any, Any), String](map.iterator.toList, { case (k, v) => k.toString }).map { case (k, v) => 157 | quote(k.toString) + ":" + build(v).body 158 | }.mkString("{", ",", "}") 159 | case x: JsonSerializable => x.toJson() 160 | case x => 161 | quote(x.toString) 162 | } 163 | JsonQuoted(rv) 164 | } 165 | 166 | /** 167 | * Parses a JSON String representation into its native Scala reprsentation. 168 | */ 169 | def parse(s: String): Any = (new JsonParser).parse(s) 170 | } 171 | 172 | 173 | /** 174 | * Wrapper for the JSON string representation of a data structure. This class exists to 175 | * allow objects to be converted into JSON, attached to another data structure, and not 176 | * re-encoded. 177 | */ 178 | case class JsonQuoted(body: String) { 179 | override def toString = body 180 | } 181 | -------------------------------------------------------------------------------- /src/main/scala/com/twitter/json/extensions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 Robey Pointer 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.twitter.json 18 | 19 | import scala.util.matching.Regex 20 | 21 | // These are Robey's extensions from Configgy and when Configgy for 2.8 is done, 22 | // we'll move back to using it. 23 | final class ConfiggyString(wrapped: String) { 24 | /** 25 | * For every section of a string that matches a regular expression, call 26 | * a function to determine a replacement (as in python's 27 | * `re.sub`). The function will be passed the Matcher object 28 | * corresponding to the substring that matches the pattern, and that 29 | * substring will be replaced by the function's result. 30 | * 31 | * For example, this call: 32 | * 33 | * "ohio".regexSub("""h.""".r) { m => "n" } 34 | * 35 | * will return the string `"ono"`. 36 | * 37 | * The matches are found using `Matcher.find()` and so 38 | * will obey all the normal java rules (the matches will not overlap, 39 | * etc). 40 | * 41 | * @param re the regex pattern to replace 42 | * @param replace a function that takes Regex.MatchData objects and 43 | * returns a string to substitute 44 | * @return the resulting string with replacements made 45 | */ 46 | def regexSub(re: Regex)(replace: (Regex.MatchData => String)): String = { 47 | var offset = 0 48 | var out = new StringBuilder 49 | 50 | for (m <- re.findAllIn(wrapped).matchData) { 51 | if (m.start > offset) { 52 | out.append(wrapped.substring(offset, m.start)) 53 | } 54 | 55 | out.append(replace(m)) 56 | offset = m.end 57 | } 58 | 59 | if (offset < wrapped.length) { 60 | out.append(wrapped.substring(offset)) 61 | } 62 | out.toString 63 | } 64 | 65 | private val QUOTE_RE = "[\u0000-\u001f\u007f-\uffff\\\\\"]".r 66 | 67 | /** 68 | * Quote a string so that unprintable chars (in ASCII) are represented by 69 | * C-style backslash expressions. For example, a raw linefeed will be 70 | * translated into "\n". Control codes (anything below 0x20) 71 | * and unprintables (anything above 0x7E) are turned into either 72 | * "\xHH" or "\\uHHHH" expressions, depending on 73 | * their range. Embedded backslashes and double-quotes are also quoted. 74 | * 75 | * @return a quoted string, suitable for ASCII display 76 | */ 77 | def quoteC(): String = { 78 | regexSub(QUOTE_RE) { m => 79 | m.matched.charAt(0) match { 80 | case '\r' => "\\r" 81 | case '\n' => "\\n" 82 | case '\t' => "\\t" 83 | case '"' => "\\\"" 84 | case '\\' => "\\\\" 85 | case c => 86 | if (c <= 255) { 87 | "\\x%02x" format c.toInt 88 | } else { 89 | "\\u%04x" format c.toInt 90 | } 91 | } 92 | } 93 | } 94 | 95 | // we intentionally don't unquote "\$" here, so it can be used to escape interpolation later. 96 | private val UNQUOTE_RE = """\\(u[\dA-Fa-f]{4}|x[\dA-Fa-f]{2}|[/rnt\"\\])""".r 97 | 98 | /** 99 | * Unquote an ASCII string that has been quoted in a style like 100 | * {@link #quoteC} and convert it into a standard unicode string. 101 | * "\\uHHHH" and "\xHH" expressions are unpacked 102 | * into unicode characters, as well as "\r", "\n", 103 | * "\t", "\\", and '\"'. 104 | * 105 | * @return an unquoted unicode string 106 | */ 107 | def unquoteC() = { 108 | def unhex(s: String): Char = Integer.valueOf(s, 16).intValue.toChar 109 | regexSub(UNQUOTE_RE) { m => 110 | val ch = m.group(1).charAt(0) match { 111 | case 'u' | 'x' => unhex(m.group(1) drop 1) 112 | case 'r' => '\r' 113 | case 'n' => '\n' 114 | case 't' => '\t' 115 | case x => x 116 | } 117 | ch.toString 118 | } 119 | } 120 | 121 | /** 122 | * Turn a string of hex digits into a byte array. This does the exact 123 | * opposite of `Array[Byte]#hexlify`. 124 | */ 125 | def unhexlify(): Array[Byte] = { 126 | val buffer = new Array[Byte](wrapped.length / 2) 127 | for (i <- 0.until(wrapped.length, 2)) { 128 | buffer(i/2) = Integer.parseInt(wrapped.substring(i, i+2), 16).toByte 129 | } 130 | buffer 131 | } 132 | } 133 | 134 | 135 | final class ConfiggyByteArray(wrapped: Array[Byte]) { 136 | /** 137 | * Turn an Array[Byte] into a string of hex digits. 138 | */ 139 | def hexlify(): String = { 140 | val out = new StringBuffer 141 | for (b <- wrapped) { 142 | val s = (b.toInt & 0xff).toHexString 143 | if (s.length < 2) { 144 | out append '0' 145 | } 146 | out append s 147 | } 148 | out.toString 149 | } 150 | } 151 | 152 | 153 | object extensions { 154 | implicit def stringToConfiggyString(s: String): ConfiggyString = new ConfiggyString(s) 155 | implicit def byteArrayToConfiggyByteArray(b: Array[Byte]): ConfiggyByteArray = new ConfiggyByteArray(b) 156 | } -------------------------------------------------------------------------------- /src/test/scala/com/twitter/json/JsonSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.twitter.json 18 | 19 | import extensions._ 20 | import org.specs._ 21 | import scala.collection.immutable 22 | 23 | 24 | class JsonSpec extends Specification { 25 | "Json" should { 26 | "quote strings" in { 27 | "unicode within latin-1" in { 28 | Json.quote("hello\n\u009f") mustEqual "\"hello\\n\\u009f\"" 29 | } 30 | 31 | "unicode outside of latin-1 (the word Tokyo)" in { 32 | Json.quote("\u6771\u4eac") mustEqual "\"\\u6771\\u4eac\"" 33 | } 34 | 35 | "string containing unicode outside of the BMP (using UTF-16 surrogate pairs)" in { 36 | // NOTE: The json.org spec is unclear on how to handle supplementary characters. 37 | val ridiculous = new java.lang.StringBuilder() 38 | ridiculous.appendCodePoint(0xfe03e) 39 | Json.quote(ridiculous.toString) mustEqual "\"\\udbb8\\udc3e\"" 40 | } 41 | 42 | "xml" in { 43 | Json.quote("sucks") mustEqual "\"sucks<\\/xml>\"" 44 | } 45 | 46 | "nested objects" in { 47 | Json.build(Json.build(List(1, 2))).toString mustEqual "[1,2]" 48 | // If this triggers, it means you are excessively escaping, sucker. 49 | Json.build(Json.build(List(1, 2))).toString must_!= "\"[1,2]\"" 50 | } 51 | } 52 | 53 | "parse strings" in { 54 | "double slashes like one finds in URLs" in { 55 | Json.parse("""["hey! http:\/\/www.lollerskates.com"]""") mustEqual 56 | List("hey! http://www.lollerskates.com") 57 | } 58 | 59 | "quoted newline" in { 60 | Json.parse("""["hi\njerk"]""") mustEqual 61 | List("hi\njerk") 62 | } 63 | 64 | "empty string" in { 65 | Json.parse("""[""]""") mustEqual List("") 66 | } 67 | 68 | "quoted quote" in { 69 | Json.parse("""["x\"x"]""") mustEqual 70 | List("x\"x") 71 | } 72 | 73 | "accept unquoted DEL char, as isn't considered control char in Json spec" in { 74 | //Json.parse("""["A^?B"]""") mustEqual List("A^?B") 75 | Json.parse("[\"A\u007fB\"]") mustEqual List("A\u007fB") 76 | } 77 | 78 | "parse escaped string thing followed by whitespace" in { 79 | Json.parse("[\"\\u2603 q\"]") mustEqual List("\u2603 q") 80 | Json.parse("[\"\\t q\"]") mustEqual List("\t q") 81 | } 82 | 83 | "parse unicode outside of the BMP" in { 84 | Json.parse("[\"\\udbb8\\udc3e\"]") mustEqual List(new String(Character.toChars(0x0FE03E))) 85 | } 86 | 87 | "does not strip leading whitespace" in { 88 | Json.parse("""[" f"]""") mustEqual List(" f") 89 | } 90 | 91 | "parse escaped backspace at end of string" in { 92 | Json.parse("""["\\", "\\"]""") mustEqual List("""\""", """\""") 93 | } 94 | 95 | "parse long string" in { 96 | Json.parse("{ \"long string\":\"" + (1 to 1000).map(x=>"That will be a long string").mkString + "\" } ") must 97 | not throwA(new Exception) 98 | } 99 | } 100 | 101 | "parse numbers" in { 102 | "floating point numbers" in { 103 | Json.parse("[1.42]") mustEqual List(BigDecimal("1.42")) 104 | } 105 | 106 | "floating point with exponent" in { 107 | Json.parse("[1.42e10]") mustEqual List(BigDecimal("1.42e10")) 108 | } 109 | 110 | "integer with exponent" in { 111 | Json.parse("[42e10]") mustEqual List(BigDecimal("42e10")) 112 | } 113 | 114 | "integer numbers" in { 115 | Json.parse("[42]") mustEqual List(42) 116 | } 117 | } 118 | 119 | "parse maps" in { 120 | "empty map" in { 121 | Json.parse("{}") mustEqual Map() 122 | } 123 | 124 | "empty list" in { 125 | Json.parse("{\"nil\":[]}") mustEqual Map("nil" -> Nil) 126 | } 127 | 128 | "empty map as value" in { 129 | Json.parse("{\"empty\":{}}") mustEqual Map("empty" -> Map()) 130 | } 131 | 132 | "simple map" in { 133 | Json.parse("{\"user_id\": 1554, \"message\": \"your phone is being turned off.\"}") mustEqual 134 | Map("user_id" -> 1554, "message" -> "your phone is being turned off.") 135 | } 136 | 137 | "simple map with long" in { 138 | Json.parse("{\"user_id\": 1554, \"status_id\": 9015551486 }") mustEqual 139 | Map("user_id" -> 1554, "status_id" -> 9015551486L) 140 | } 141 | 142 | "map with map" in { 143 | Json.parse("{\"name\":\"nathaniel\",\"status\":{\"text\":\"i like to dance!\"," + 144 | "\"created_at\":666},\"zipcode\":94103}") mustEqual 145 | Map("name" -> "nathaniel", 146 | "status" -> Map("text" -> "i like to dance!", 147 | "created_at" -> 666), 148 | "zipcode" -> 94103) 149 | } 150 | 151 | "map with list" in { 152 | Json.parse("{\"names\":[\"nathaniel\",\"brittney\"]}") mustEqual 153 | Map("names" -> List("nathaniel", "brittney")) 154 | } 155 | 156 | "map with two lists" in { 157 | Json.parse("{\"names\":[\"nathaniel\",\"brittney\"],\"ages\":[4,7]}") mustEqual 158 | Map("names" -> List("nathaniel", "brittney"), 159 | "ages" -> List(4, 7)) 160 | } 161 | 162 | "map with list, boolean and map" in { 163 | Json.parse("{\"names\":[\"nathaniel\",\"brittney\"],\"adults\":false," + 164 | "\"ages\":{\"nathaniel\":4,\"brittney\":7}}") mustEqual 165 | Map("names" -> List("nathaniel", "brittney"), 166 | "adults" -> false, 167 | "ages" -> Map("nathaniel" -> 4, 168 | "brittney" -> 7)) 169 | } 170 | } 171 | 172 | "build maps" in { 173 | "empty map" in { 174 | Json.build(Map()).toString mustEqual "{}" 175 | } 176 | 177 | "empty list" in { 178 | Json.build(Map("nil" -> Nil)).toString mustEqual "{\"nil\":[]}" 179 | } 180 | 181 | "empty map as value" in { 182 | Json.build(Map("empty" -> Map())).toString mustEqual "{\"empty\":{}}" 183 | } 184 | 185 | "simple map" in { 186 | Json.build(Map("name" -> "nathaniel", 187 | "likes" -> "to dance", 188 | "age" -> 4)).toString mustEqual 189 | "{\"age\":4,\"likes\":\"to dance\",\"name\":\"nathaniel\"}" 190 | 191 | Json.build(List(1, 2, 3)).toString mustEqual "[1,2,3]" 192 | } 193 | 194 | "simple map with long" in { 195 | Json.build(Map("user_id" -> 1554, "status_id" -> 9015551486L)).toString mustEqual 196 | "{\"status_id\":9015551486,\"user_id\":1554}" 197 | } 198 | 199 | "Map with nested Map" in { 200 | Json.build(Map("name" -> "nathaniel", 201 | "status" -> Map("text" -> "i like to dance!", 202 | "created_at" -> 666), 203 | "zipcode" -> 94103)).toString mustEqual 204 | "{\"name\":\"nathaniel\",\"status\":{\"created_at\":666,\"text\":\"i like to dance!\"}," + 205 | "\"zipcode\":94103}" 206 | } 207 | 208 | "immutable maps" in { 209 | import scala.collection.immutable.Map 210 | 211 | "nested" in { 212 | Json.build(Map("name" -> "nathaniel", 213 | "status" -> Map("created_at" -> 666, "text" -> "i like to dance!"), 214 | "zipcode" -> 94103)).toString mustEqual 215 | "{\"name\":\"nathaniel\",\"status\":{\"created_at\":666,\"text\":\"i like to dance!\"}," + 216 | "\"zipcode\":94103}" 217 | } 218 | 219 | "appended" in { 220 | val statusMap = Map("status" -> Map("text" -> "i like to dance!", 221 | "created_at" -> 666)) 222 | Json.build(Map.empty ++ 223 | Map("name" -> "nathaniel") ++ 224 | statusMap ++ 225 | Map("zipcode" -> 94103)).toString mustEqual 226 | "{\"name\":\"nathaniel\",\"status\":{\"created_at\":666,\"text\":\"i like to dance!\"}," + 227 | "\"zipcode\":94103}" 228 | 229 | } 230 | } 231 | 232 | "mutable maps" in { 233 | "nested" in { 234 | import scala.collection.mutable.Map 235 | 236 | "literal map" in { 237 | val map = Map("name" -> "nathaniel", 238 | "status" -> Map("text" -> "i like to dance!", 239 | "created_at" -> 666), 240 | "zipcode" -> 94103) 241 | 242 | 243 | val output = Json.build(map).toString 244 | val rehydrated = Json.parse(output) 245 | 246 | rehydrated mustEqual map 247 | } 248 | 249 | "appended" in { 250 | val statusMap = Map("status" -> Map("text" -> "i like to dance!", 251 | "created_at" -> 666)) 252 | 253 | val nestedMap = Map[String,Any]() ++ 254 | Map("name" -> "nathaniel") ++ 255 | statusMap ++ 256 | Map("zipcode" -> 94103) 257 | 258 | val output = Json.build(nestedMap).toString 259 | val rehydrated = Json.parse(output) 260 | 261 | rehydrated mustEqual nestedMap 262 | } 263 | } 264 | } 265 | 266 | "map with list" in { 267 | Json.build(Map("names" -> List("nathaniel", "brittney"))).toString mustEqual 268 | "{\"names\":[\"nathaniel\",\"brittney\"]}" 269 | } 270 | 271 | "map with two lists" in { 272 | Json.build(Map("names" -> List("nathaniel", "brittney"), 273 | "ages" -> List(4, 7))).toString mustEqual 274 | "{\"ages\":[4,7],\"names\":[\"nathaniel\",\"brittney\"]}" 275 | } 276 | 277 | "map with list, boolean and map" in { 278 | Json.build(Map("names" -> List("nathaniel", "brittney"), 279 | "adults" -> false, 280 | "ages" -> Map("nathaniel" -> 4, 281 | "brittney" -> 7))).toString mustEqual 282 | "{\"adults\":false," + 283 | "\"ages\":{\"brittney\":7,\"nathaniel\":4}," + 284 | "\"names\":[\"nathaniel\",\"brittney\"]}" 285 | } 286 | } 287 | 288 | "parse lists" in { 289 | "empty list" in { 290 | Json.parse("[]") mustEqual Nil 291 | } 292 | 293 | "empty empty list" in { 294 | Json.parse("[[]]") mustEqual List(Nil) 295 | } 296 | 297 | "list with empty Map" in { 298 | Json.parse("[{}]") mustEqual List(Map()) 299 | } 300 | 301 | "simple list" in { 302 | Json.parse("[\"id\", 1]") mustEqual List("id", 1) 303 | } 304 | 305 | "nested list" in { 306 | Json.parse("[\"more lists!\",[1,2,\"three\"]]") mustEqual 307 | List("more lists!", List(1, 2, "three")) 308 | } 309 | 310 | "list with map" in { 311 | Json.parse("[\"maptastic!\",{\"1\":2}]") mustEqual 312 | List("maptastic!", Map("1" -> 2)) 313 | } 314 | 315 | "list with two maps" in { 316 | Json.parse("[{\"1\":2},{\"3\":4}]") mustEqual 317 | List(Map("1" -> 2), Map("3" -> 4)) 318 | } 319 | 320 | "list with list, boolean, map" in { 321 | Json.parse("{\"names\":[\"nathaniel\",\"brittney\"],\"adults\":false," + 322 | "\"ages\":{\"nathaniel\":4,\"brittney\":7}}") mustEqual 323 | Map("names" -> List("nathaniel", "brittney"), 324 | "adults" -> false, 325 | "ages" -> Map("nathaniel" -> 4, 326 | "brittney" -> 7)) 327 | } 328 | 329 | "list with map containing list" in { 330 | Json.parse("[{\"1\":[2,3]}]") mustEqual 331 | List(Map("1" -> List(2, 3))) 332 | } 333 | 334 | "list with map containing map" in { 335 | Json.parse("[{\"1\":{\"2\":\"3\"}}]") mustEqual 336 | List(Map("1" -> Map("2" -> "3"))) 337 | } 338 | 339 | "list in the middle" in { 340 | Json.parse("""{"JobWithTasks":{"tasks":[{"Add":{"updated_at":12,"position":13}}],"error_count":1}}""") mustEqual 341 | Map("JobWithTasks" -> Map("tasks" -> List(Map("Add" -> Map("updated_at" -> 12, "position" -> 13))), "error_count" -> 1)) 342 | } 343 | } 344 | 345 | "build lists" in { 346 | "empty empty list" in { 347 | Json.build(List(Nil)).toString mustEqual "[[]]" 348 | } 349 | 350 | "list with empty Map" in { 351 | Json.build(List(Map())).toString mustEqual "[{}]" 352 | } 353 | 354 | "simple list" in { 355 | Json.build(List("id", 1)).toString mustEqual "[\"id\",1]" 356 | } 357 | 358 | "nested list" in { 359 | Json.build(List("more lists!", List(1, 2, "three"))).toString mustEqual 360 | "[\"more lists!\",[1,2,\"three\"]]" 361 | } 362 | 363 | "list with map" in { 364 | Json.build(List("maptastic!", Map("1" -> 2))).toString mustEqual 365 | "[\"maptastic!\",{\"1\":2}]" 366 | } 367 | 368 | "list with two maps" in { 369 | Json.build(List(Map("1" -> 2), Map("3" -> 4))).toString mustEqual 370 | "[{\"1\":2},{\"3\":4}]" 371 | } 372 | 373 | "list with map containing list" in { 374 | Json.build(List(Map("1" -> List(2, 3)))).toString mustEqual 375 | "[{\"1\":[2,3]}]" 376 | } 377 | 378 | "list with map containing map" in { 379 | Json.build(List(Map("1" -> Map("2" -> "3")))).toString mustEqual 380 | "[{\"1\":{\"2\":\"3\"}}]" 381 | } 382 | } 383 | 384 | "build numbers" in { 385 | Json.build(List(42, 23L, 1.67, BigDecimal("1.67456352431287348917591342E+50"))).toString mustEqual "[42,23,1.67,1.67456352431287348917591342E+50]"; 386 | Json.build(List(0.0, 5.25)).toString mustEqual "[0.0,5.25]" 387 | } 388 | 389 | "arrays" in { 390 | "simple arrays can be encoded" in { 391 | Json.build(Array(0, 1)).toString mustEqual "[0,1]" 392 | } 393 | 394 | "nested" in { 395 | "inside of arrays" in { 396 | Json.build(Array(Array(0, 1), 2.asInstanceOf[AnyRef])).toString mustEqual "[[0,1],2]" 397 | Json.build(Array(Array(0, 1), Array(2, 3))).toString mustEqual 398 | "[[0,1],[2,3]]" 399 | } 400 | 401 | "inside of Lists" in { 402 | Json.build(List(Array(0, 1))).toString mustEqual "[[0,1]]" 403 | Json.build(List(Array(0, 1), Array(2, 3))).toString mustEqual "[[0,1],[2,3]]" 404 | } 405 | } 406 | 407 | "maps" in { 408 | "can contain arrays" in { 409 | Json.build(List(Map("1" -> Array(0, 2)))).toString mustEqual 410 | "[{\"1\":[0,2]}]" 411 | } 412 | 413 | "can be contained in arrays" in { 414 | Json.build(Array(Map("1" -> 2))).toString mustEqual "[{\"1\":2}]" 415 | } 416 | } 417 | } 418 | 419 | "build JsonSerializable objects" in { 420 | val obj = new JsonSerializable { 421 | def toJson() = "\"abracadabra\"" 422 | } 423 | Json.build(List(obj, 23)).toString mustEqual "[\"abracadabra\",23]" 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/test/scala/com/twitter/json/TestRunner.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2009 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.twitter.json 18 | 19 | import org.specs.runner.SpecsFileRunner 20 | 21 | object TestRunner extends SpecsFileRunner("src/test/scala/**/*.scala", ".*", 22 | System.getProperty("system", ".*"), System.getProperty("example", ".*")) 23 | --------------------------------------------------------------------------------