summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDerek Chen-Beker <dchenbecker@gmail.com>2010-10-08 20:22:13 +0000
committerDerek Chen-Beker <dchenbecker@gmail.com>2010-10-08 20:22:13 +0000
commit908ed2f29f4dbe03bb2ff73341a74c524f44f964 (patch)
tree6e7ba5f87f82d0d9ecfd018de561127d5376cfb2
parent4af97e33e7f60e61df1483af3605050a3af5dfb6 (diff)
downloadscala-908ed2f29f4dbe03bb2ff73341a74c524f44f964.tar.gz
scala-908ed2f29f4dbe03bb2ff73341a74c524f44f964.tar.bz2
scala-908ed2f29f4dbe03bb2ff73341a74c524f44f964.zip
Made some adjustments to toString formatting of...
Made some adjustments to toString formatting of JSON Closes #3605 Hopefully this is the last time I have to close this ticket. In addition to default behavior, the end user can specify their own JSON value formatting function if they want to customize it.
-rw-r--r--src/library/scala/util/parsing/json/Parser.scala76
-rw-r--r--test/files/run/json.check30
-rw-r--r--test/files/run/json.scala88
3 files changed, 154 insertions, 40 deletions
diff --git a/src/library/scala/util/parsing/json/Parser.scala b/src/library/scala/util/parsing/json/Parser.scala
index ce9c1dd3fe..b7c338b3be 100644
--- a/src/library/scala/util/parsing/json/Parser.scala
+++ b/src/library/scala/util/parsing/json/Parser.scala
@@ -19,14 +19,81 @@ import scala.util.parsing.combinator.lexical._
*
* @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org">
*/
-sealed abstract class JSONType
+sealed abstract class JSONType {
+ /**
+ * This version of toString allows you to provide your own value
+ * formatter.
+ */
+ def toString (formatter : JSONFormat.ValueFormatter) : String
+
+ /**
+ * Returns a String representation of this JSON value
+ * using the JSONFormat.defaultFormatter.
+ */
+ override def toString = toString(JSONFormat.defaultFormatter)
+}
+
+/**
+ * This object defines functions that are used when converting JSONType
+ * values into String representations. Mostly this is concerned with
+ * proper quoting of strings.
+ *
+ * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org">
+ */
+object JSONFormat {
+ /**
+ * This type defines a function that can be used to
+ * format values into JSON format.
+ */
+ type ValueFormatter = Any => String
+
+ /**
+ * The default formatter used by the library. You can
+ * provide your own with the toString calls on
+ * JSONObject and JSONArray instances.
+ */
+ val defaultFormatter : ValueFormatter = (x : Any) => x match {
+ case s : String => "\"" + quoteString(s) + "\""
+ case jo : JSONObject => jo.toString(defaultFormatter)
+ case ja : JSONArray => ja.toString(defaultFormatter)
+ case other => other.toString
+ }
+
+ /**
+ * This function can be used to properly quote Strings
+ * for JSON output.
+ */
+ def quoteString (s : String) : String =
+ s.map {
+ case '"' => "\\\""
+ case '\\' => "\\\\"
+ case '/' => "\\/"
+ case '\b' => "\\b"
+ case '\f' => "\\f"
+ case '\n' => "\\n"
+ case '\r' => "\\r"
+ case '\t' => "\\t"
+ /* We'll unicode escape any control characters. These include:
+ * 0x0 -> 0x1f : ASCII Control (C0 Control Codes)
+ * 0x7f : ASCII DELETE
+ * 0x80 -> 0x9f : C1 Control Codes
+ *
+ * Per RFC4627, section 2.5, we're not technically required to
+ * encode the C1 codes, but we do to be safe.
+ */
+ case c if ((c >= '\u0000' && c <= '\u001f') || (c >= '\u007f' && c <= '\u009f')) => "\\u%04x".format(c: Int)
+ case c => c
+ }.mkString
+}
/**
* Represents a JSON Object (map).
+ *
* @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org">
*/
-case class JSONObject (obj : Map[Any,Any]) extends JSONType {
- override def toString = "{" + obj.map({ case (k,v) => k + " : " + v }).mkString(", ") + "}"
+case class JSONObject (obj : Map[String,Any]) extends JSONType {
+ def toString (formatter : JSONFormat.ValueFormatter) =
+ "{" + obj.map({ case (k,v) => formatter(k.toString) + " : " + formatter(v) }).mkString(", ") + "}"
}
/**
@@ -34,7 +101,8 @@ case class JSONObject (obj : Map[Any,Any]) extends JSONType {
* @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org">
*/
case class JSONArray (list : List[Any]) extends JSONType {
- override def toString = "[" + list.mkString(", ") + "]"
+ def toString (formatter : JSONFormat.ValueFormatter) =
+ "[" + list.map(formatter).mkString(", ") + "]"
}
/**
diff --git a/test/files/run/json.check b/test/files/run/json.check
index 021214beaa..d4d2b41658 100644
--- a/test/files/run/json.check
+++ b/test/files/run/json.check
@@ -1,17 +1,21 @@
-Passed: Map(name -> value)
-Passed: Map(name -> va1ue)
-Passed: Map(name -> Map(name1 -> va1ue1, name2 -> va1ue2))
-Passed: Map(name -> ")
-Passed: Map(function -> add_symbol)
-Passed: List(Map(a -> team), Map(b -> 52.0))
-Passed: Map()
-Passed: List()
-Passed: List(4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0)
-Passed: Map(age -> 0.0)
+Passed compare: {"name" : "value"}
+Passed compare: {"name" : "va1ue"}
+Passed compare: {"name" : {"name1" : "va1ue1", "name2" : "va1ue2"}}
+Passed parse : {"name" : "\""}
+Passed compare: Map(function -> add_symbol)
+Passed compare: [{"a" : "team"}, {"b" : 52.0}]
+Passed compare: Map()
+Passed compare: List()
+Passed compare: [4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0]
+Passed parse : {"age" : 0.0}
+Passed compare: {"name" : "va1ue"}
+Passed compare: {"name" : {"name1" : "va1ue1", "name2" : "va1ue2"}}
+Passed compare: [4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0]
+Passed compare: {"\u006e\u0061\u006d\u0065" : "\u0076\u0061\u006c"}
-Passed: Map(firstName -> John, lastName -> Smith, address -> Map(streetAddress -> 21 2nd Street, city -> New York, state -> NY, postalCode -> 10021.0), phoneNumbers -> List(212 732-1234, 646 123-4567))
+Passed compare: Map(firstName -> John, lastName -> Smith, address -> Map(streetAddress -> 21 2nd Street, city -> New York, state -> NY, postalCode -> 10021.0), phoneNumbers -> List(212 732-1234, 646 123-4567))
-Passed: Map(addresses -> List(Map(format -> us, type -> work, value -> 1234 Main StnSpringfield, TX 78080-1216), Map(format -> us, type -> home, value -> 5678 Main StnSpringfield, TX 78080-1316)), emailaddrs -> List(Map(type -> work, value -> kelly@seankelly.biz), Map(pref -> 1.0, type -> home, value -> kelly@seankelly.tv)), fullname -> Sean Kelly, org -> SK Consulting, telephones -> List(Map(pref -> 1.0, type -> work, value -> +1 214 555 1212), Map(type -> fax, value -> +1 214 555 1213), Map(type -> mobile, value -> +1 214 555 1214)), urls -> List(Map(type -> work, value -> http://seankelly.biz/), Map(type -> home, value -> http://seankelly.tv/)))
+Passed parse : {"addresses" : [{"format" : "us", "type" : "work", "value" : "1234 Main StnSpringfield, TX 78080-1216"}, {"format" : "us", "type" : "home", "value" : "5678 Main StnSpringfield, TX 78080-1316"}], "emailaddrs" : [{"type" : "work", "value" : "kelly@seankelly.biz"}, {"pref" : 1.0, "type" : "home", "value" : "kelly@seankelly.tv"}], "fullname" : "Sean Kelly", "org" : "SK Consulting", "telephones" : [{"pref" : 1.0, "type" : "work", "value" : "+1 214 555 1212"}, {"type" : "fax", "value" : "+1 214 555 1213"}, {"type" : "mobile", "value" : "+1 214 555 1214"}], "urls" : [{"type" : "work", "value" : "http:\/\/seankelly.biz\/"}, {"type" : "home", "value" : "http:\/\/seankelly.tv\/"}]}
-Passed: Map(web-app -> Map(servlet -> List(Map(init-param -> Map(cachePackageTagsRefresh -> 60.0, cachePackageTagsStore -> 200.0, cachePackageTagsTrack -> 200.0, cachePagesDirtyRead -> 10.0, cachePagesRefresh -> 10.0, cachePagesStore -> 100.0, cachePagesTrack -> 200.0, cacheTemplatesRefresh -> 15.0, cacheTemplatesStore -> 50.0, cacheTemplatesTrack -> 100.0, configGlossary:adminEmail -> ksm@pobox.com, configGlossary:installationAt -> Philadelphia, PA, configGlossary:poweredBy -> Cofax, configGlossary:poweredByIcon -> /images/cofax.gif, configGlossary:staticPath -> /content/static, dataStoreClass -> org.cofax.SqlDataStore, dataStoreConnUsageLimit -> 100.0, dataStoreDriver -> com.microsoft.jdbc.sqlserver.SQLServerDriver, dataStoreInitConns -> 10.0, dataStoreLogFile -> /usr/local/tomcat/logs/datastore.log, dataStoreLogLevel -> debug, dataStoreMaxConns -> 100.0, dataStoreName -> cofax, dataStorePassword -> dataStoreTestQuery, dataStoreTestQuery -> SET NOCOUNT ON;select test='test';, dataStoreUrl -> jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon, dataStoreUser -> sa, defaultFileTemplate -> articleTemplate.htm, defaultListTemplate -> listTemplate.htm, jspFileTemplate -> articleTemplate.jsp, jspListTemplate -> listTemplate.jsp, maxUrlLength -> 500.0, redirectionClass -> org.cofax.SqlRedirection, searchEngineFileTemplate -> forSearchEngines.htm, searchEngineListTemplate -> forSearchEnginesList.htm, searchEngineRobotsDb -> WEB-INF/robots.db, templateLoaderClass -> org.cofax.FilesTemplateLoader, templateOverridePath -> , templatePath -> templates, templateProcessorClass -> org.cofax.WysiwygTemplate, useDataStore -> true, useJSP -> false), servlet-class -> org.cofax.cds.CDSServlet, servlet-name -> cofaxCDS), Map(init-param -> Map(mailHost -> mail1, mailHostOverride -> mail2), servlet-class -> org.cofax.cds.EmailServlet, servlet-name -> cofaxEmail), Map(servlet-class -> org.cofax.cds.AdminServlet, servlet-name -> cofaxAdmin), Map(servlet-class -> org.cofax.cds.FileServlet, servlet-name -> fileServlet), Map(init-param -> Map(adminGroupID -> 4.0, betaServer -> true, dataLog -> 1.0, dataLogLocation -> /usr/local/tomcat/logs/dataLog.log, dataLogMaxSize -> , fileTransferFolder -> /usr/local/tomcat/webapps/content/fileTransferFolder, log -> 1.0, logLocation -> /usr/local/tomcat/logs/CofaxTools.log, logMaxSize -> , lookInContext -> 1.0, removePageCache -> /content/admin/remove?cache=pages&id=, removeTemplateCache -> /content/admin/remove?cache=templates&id=, templatePath -> toolstemplates/), servlet-class -> org.cofax.cms.CofaxToolsServlet, servlet-name -> cofaxTools)), servlet-mapping -> Map(cofaxAdmin -> /admin/*, cofaxCDS -> /, cofaxEmail -> /cofaxutil/aemail/*, cofaxTools -> /tools/*, fileServlet -> /static/*), taglib -> Map(taglib-location -> /WEB-INF/tlds/cofax.tld, taglib-uri -> cofax.tld)))
+Passed parse : {"web-app" : {"servlet" : [{"init-param" : {"cachePackageTagsRefresh" : 60.0, "cachePackageTagsStore" : 200.0, "cachePackageTagsTrack" : 200.0, "cachePagesDirtyRead" : 10.0, "cachePagesRefresh" : 10.0, "cachePagesStore" : 100.0, "cachePagesTrack" : 200.0, "cacheTemplatesRefresh" : 15.0, "cacheTemplatesStore" : 50.0, "cacheTemplatesTrack" : 100.0, "configGlossary:adminEmail" : "ksm@pobox.com", "configGlossary:installationAt" : "Philadelphia, PA", "configGlossary:poweredBy" : "Cofax", "configGlossary:poweredByIcon" : "\/images\/cofax.gif", "configGlossary:staticPath" : "\/content\/static", "dataStoreClass" : "org.cofax.SqlDataStore", "dataStoreConnUsageLimit" : 100.0, "dataStoreDriver" : "com.microsoft.jdbc.sqlserver.SQLServerDriver", "dataStoreInitConns" : 10.0, "dataStoreLogFile" : "\/usr\/local\/tomcat\/logs\/datastore.log", "dataStoreLogLevel" : "debug", "dataStoreMaxConns" : 100.0, "dataStoreName" : "cofax", "dataStorePassword" : "dataStoreTestQuery", "dataStoreTestQuery" : "SET NOCOUNT ON;select test='test';", "dataStoreUrl" : "jdbc:microsoft:sqlserver:\/\/LOCALHOST:1433;DatabaseName=goon", "dataStoreUser" : "sa", "defaultFileTemplate" : "articleTemplate.htm", "defaultListTemplate" : "listTemplate.htm", "jspFileTemplate" : "articleTemplate.jsp", "jspListTemplate" : "listTemplate.jsp", "maxUrlLength" : 500.0, "redirectionClass" : "org.cofax.SqlRedirection", "searchEngineFileTemplate" : "forSearchEngines.htm", "searchEngineListTemplate" : "forSearchEnginesList.htm", "searchEngineRobotsDb" : "WEB-INF\/robots.db", "templateLoaderClass" : "org.cofax.FilesTemplateLoader", "templateOverridePath" : "", "templatePath" : "templates", "templateProcessorClass" : "org.cofax.WysiwygTemplate", "useDataStore" : true, "useJSP" : false}, "servlet-class" : "org.cofax.cds.CDSServlet", "servlet-name" : "cofaxCDS"}, {"init-param" : {"mailHost" : "mail1", "mailHostOverride" : "mail2"}, "servlet-class" : "org.cofax.cds.EmailServlet", "servlet-name" : "cofaxEmail"}, {"servlet-class" : "org.cofax.cds.AdminServlet", "servlet-name" : "cofaxAdmin"}, {"servlet-class" : "org.cofax.cds.FileServlet", "servlet-name" : "fileServlet"}, {"init-param" : {"adminGroupID" : 4.0, "betaServer" : true, "dataLog" : 1.0, "dataLogLocation" : "\/usr\/local\/tomcat\/logs\/dataLog.log", "dataLogMaxSize" : "", "fileTransferFolder" : "\/usr\/local\/tomcat\/webapps\/content\/fileTransferFolder", "log" : 1.0, "logLocation" : "\/usr\/local\/tomcat\/logs\/CofaxTools.log", "logMaxSize" : "", "lookInContext" : 1.0, "removePageCache" : "\/content\/admin\/remove?cache=pages&id=", "removeTemplateCache" : "\/content\/admin\/remove?cache=templates&id=", "templatePath" : "toolstemplates\/"}, "servlet-class" : "org.cofax.cms.CofaxToolsServlet", "servlet-name" : "cofaxTools"}], "servlet-mapping" : {"cofaxAdmin" : "\/admin\/*", "cofaxCDS" : "\/", "cofaxEmail" : "\/cofaxutil\/aemail\/*", "cofaxTools" : "\/tools\/*", "fileServlet" : "\/static\/*"}, "taglib" : {"taglib-location" : "\/WEB-INF\/tlds\/cofax.tld", "taglib-uri" : "cofax.tld"}}}
diff --git a/test/files/run/json.scala b/test/files/run/json.scala
index 0a141dc38c..1e0b5be94b 100644
--- a/test/files/run/json.scala
+++ b/test/files/run/json.scala
@@ -15,65 +15,107 @@ object Test extends Application {
case x => x.toString
}
+ /*
+ * This method takes input JSON values and sorts keys on objects.
+ */
def sortJSON(in : Any) : Any = in match {
case l : List[_] => l.map(sortJSON)
case m : Map[String,_] => TreeMap(m.mapElements(sortJSON).elements.toSeq : _*)
+ // For the object versions, sort their contents, ugly casts and all...
+ case JSONObject(data) => JSONObject(sortJSON(data).asInstanceOf[Map[String,Any]])
+ case JSONArray(data) => JSONArray(sortJSON(data).asInstanceOf[List[Any]])
case x => x
}
// For this one, just parsing should be considered a pass
def printJSON(given : String) {
- JSON parseFull given match {
+ JSON parseRaw given match {
case None => println("Parse failed for \"%s\"".format(given))
- case Some(parsed) => println("Passed: " + sortJSON(parsed))
+ case Some(parsed) => println("Passed parse : " + sortJSON(parsed))
}
}
- def printJSON(given : String, expected : Any) {
- JSON parseFull given match {
+ // For this usage, do a raw parse (to JSONObject/JSONArray)
+ def printJSON(given : String, expected : JSONType) {
+ printJSON(given, JSON.parseRaw, expected)
+ }
+
+ // For this usage, do a raw parse (to JSONType and subclasses)
+ def printJSONFull(given : String, expected : Any) {
+ printJSON(given, JSON.parseFull, expected)
+ }
+
+ // For this usage, do configurable parsing so that you can do raw if desired
+ def printJSON[T](given : String, parser : String => T, expected : Any) {
+ parser(given) match {
case None => println("Parse failed for \"%s\"".format(given))
case Some(parsed) => if (parsed == expected) {
- println("Passed: " + parsed)
+ println("Passed compare: " + parsed)
} else {
val eStr = sortJSON(expected).toString
val pStr = sortJSON(parsed).toString
+ stringDiff(eStr,pStr)
+ }
+ }
+ }
- // Figure out where the Strings differ and generate a marker
- val mismatchPosition = eStr.toList.zip(pStr.toList).findIndexOf({case (a,b) => a != b}) match {
- case -1 => Math.min(eStr.length, pStr.length)
+ def stringDiff (expected : String, actual : String) {
+ if (expected != actual) {
+ // Figure out where the Strings differ and generate a marker
+ val mismatchPosition = expected.toList.zip(actual.toList).findIndexOf({case (x,y) => x != y}) match {
+ case -1 => Math.min(expected.length, actual.length)
case x => x
}
val reason = (" " * mismatchPosition) + "^"
- println("Expected, got:\n %s\n %s (from \"%s\")\n %s".format(eStr, pStr, given, reason))
- }
+ println("Expected: %s\nGot : %s \n %s".format(expected, actual, reason))
+
+ } else {
+ println("Passed compare: " + actual)
}
}
+
// The library should differentiate between lower case "l" and number "1" (ticket #136)
- printJSON("{\"name\": \"value\"}", Map("name" -> "value"))
- printJSON("{\"name\": \"va1ue\"}", Map("name" -> "va1ue"))
- printJSON("{\"name\": { \"name1\": \"va1ue1\", \"name2\": \"va1ue2\" } }",
- Map("name" -> Map("name1" -> "va1ue1", "name2" -> "va1ue2")))
+ printJSON("{\"name\" : \"value\"}", JSONObject(Map("name" -> "value")))
+ printJSON("{\"name\" : \"va1ue\"}", JSONObject(Map("name" -> "va1ue")))
+ printJSON("{\"name\" : { \"name1\" : \"va1ue1\", \"name2\" : \"va1ue2\" } }",
+ JSONObject(Map("name" -> JSONObject(Map("name1" -> "va1ue1", "name2" -> "va1ue2")))))
// Unicode escapes should be handled properly
- printJSON("{\"name\": \"\\u0022\"}")
+ printJSON("{\"name\" : \"\\u0022\"}")
// The library should return a map for JSON objects (ticket #873)
- printJSON("""{"function":"add_symbol"}""", Map("function" -> "add_symbol"))
+ printJSONFull("{\"function\" : \"add_symbol\"}", Map("function" -> "add_symbol"))
// The library should recurse into arrays to find objects (ticket #2207)
- printJSON("""[{"a": "team"},{"b": 52}]""", List(Map("a" -> "team"), Map("b" -> 52.0)))
+ printJSON("[{\"a\" : \"team\"},{\"b\" : 52}]", JSONArray(List(JSONObject(Map("a" -> "team")), JSONObject(Map("b" -> 52.0)))))
// The library should differentiate between empty maps and lists (ticket #3284)
- printJSON("{}", Map())
- printJSON("[]", List())
+ printJSONFull("{}", Map())
+ printJSONFull("[]", List())
// Lists should be returned in the same order as specified
- printJSON("[4,1,3,2,6,5,8,7]", List[Double](4,1,3,2,6,5,8,7))
+ printJSON("[4,1,3,2,6,5,8,7]", JSONArray(List[Double](4,1,3,2,6,5,8,7)))
// Additional tests
printJSON("{\"age\": 0}")
+ // The library should do a proper toString representation using default and custom renderers (ticket #3605)
+ stringDiff("{\"name\" : \"va1ue\"}", JSONObject(Map("name" -> "va1ue")).toString)
+ stringDiff("{\"name\" : {\"name1\" : \"va1ue1\", \"name2\" : \"va1ue2\"}}",
+ JSONObject(Map("name" -> JSONObject(TreeMap("name1" -> "va1ue1", "name2" -> "va1ue2")))).toString)
+
+ stringDiff("[4.0, 1.0, 3.0, 2.0, 6.0, 5.0, 8.0, 7.0]", JSONArray(List[Double](4,1,3,2,6,5,8,7)).toString)
+
+ // A test method that escapes all characters in strings
+ def escapeEverything (in : Any) : String = in match {
+ case s : String => "\"" + s.map(c => "\\u%04x".format(c : Int)).mkString + "\""
+ case jo : JSONObject => jo.toString(escapeEverything)
+ case ja : JSONArray => ja.toString(escapeEverything)
+ case other => other.toString
+ }
+
+ stringDiff("{\"\\u006e\\u0061\\u006d\\u0065\" : \"\\u0076\\u0061\\u006c\"}", JSONObject(Map("name" -> "val")).toString(escapeEverything))
println
@@ -111,7 +153,7 @@ object Test extends Application {
)
- printJSON(sample1, sample1Obj)
+ printJSONFull(sample1, sample1Obj)
println
// from http://www.developer.com/lang/jscript/article.php/3596836
@@ -139,7 +181,7 @@ object Test extends Application {
{"type": "home", "value": "http://seankelly.tv/"}
]
}"""
- //println(sample2)
+
printJSON(sample2)
println
@@ -235,7 +277,7 @@ object Test extends Application {
"taglib-location": "/WEB-INF/tlds/cofax.tld"}
}
}"""
- //println(sample3)
+
printJSON(sample3)
println
}