├── .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 ""
138 | case c => 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 |
--------------------------------------------------------------------------------