diff options
author | Derek Chen-Beker <dchenbecker@gmail.com> | 2010-06-04 23:05:57 +0000 |
---|---|---|
committer | Derek Chen-Beker <dchenbecker@gmail.com> | 2010-06-04 23:05:57 +0000 |
commit | d3a747882c0332dd1316c31c500f983268bd8c8b (patch) | |
tree | 48def287f402c75cd5d2d5e9fc7632ff0257b483 | |
parent | 84b86a977ea090c66e128b26e1b7643fd50a99a1 (diff) | |
download | scala-d3a747882c0332dd1316c31c500f983268bd8c8b.tar.gz scala-d3a747882c0332dd1316c31c500f983268bd8c8b.tar.bz2 scala-d3a747882c0332dd1316c31c500f983268bd8c8b.zip |
Fix for #3284.
but in the interest of not breaking backwards compatibility, the
JSON.parse method has been marked deprecated for now.
Unit tests have been fixed so that this won't break the build this time.
-rw-r--r-- | src/library/scala/util/parsing/json/JSON.scala | 52 | ||||
-rw-r--r-- | src/library/scala/util/parsing/json/Parser.scala | 29 | ||||
-rw-r--r-- | test/files/run/json.check | 21 | ||||
-rw-r--r-- | test/files/run/json.scala | 97 |
4 files changed, 163 insertions, 36 deletions
diff --git a/src/library/scala/util/parsing/json/JSON.scala b/src/library/scala/util/parsing/json/JSON.scala index 1be8a10931..6d3761af52 100644 --- a/src/library/scala/util/parsing/json/JSON.scala +++ b/src/library/scala/util/parsing/json/JSON.scala @@ -41,8 +41,33 @@ object JSON extends Parser { * * @param input the given JSON string. * @return an optional list of of elements. + * + * @deprecated Use parseFull or parseRaw as needed. + */ + def parse(input: String): Option[List[Any]] = parseRaw(input).map(unRaw).flatMap({ + case l : List[_] => Some(l) + case _ => None + }) + + /** + * This method converts "raw" results back into the original, deprecated + * form. */ - def parse(input: String): Option[List[Any]] = + private def unRaw (in : Any) : Any = in match { + case JSONObject(obj) => obj.map({ case (k,v) => (k,unRaw(v))}).toList + case JSONArray(list) => list.map(unRaw) + case x => x + } + + /** + * Parse the given JSON string and return a list of elements. If the + * string is a JSON object it will be a JSONObject. If it's a JSON + * array it will be be a JSONArray. + * + * @param input the given JSON string. + * @return an optional JSONType element. + */ + def parseRaw(input : String) : Option[JSONType] = phrase(root)(new lexical.Scanner(input)) match { case Success(result, _) => Some(result) case _ => None @@ -57,7 +82,7 @@ object JSON extends Parser { * @return an optional list or map. */ def parseFull(input: String): Option[Any] = - parse(input) match { + parseRaw(input) match { case Some(data) => Some(resolveType(data)) case None => None } @@ -66,23 +91,12 @@ object JSON extends Parser { * A utility method to resolve a parsed JSON list into objects or * arrays. See the parse method for details. */ - def resolveType(input: List[_]): Any = { - var objMap = Map[String, Any]() - - if (input.forall { - case (key: String, value: List[_]) => - objMap = objMap.+[Any](key -> resolveType(value)) - true - case (key : String, value) => - objMap += key -> value - true - case _ => false - }) objMap - else - input.map { - case l : List[_] => resolveType(l) - case x => x - } + def resolveType(input: Any): Any = input match { + case JSONObject(data) => data.transform { + case (k,v) => resolveType(v) + } + case JSONArray(data) => data.map(resolveType) + case x => x } /** diff --git a/src/library/scala/util/parsing/json/Parser.scala b/src/library/scala/util/parsing/json/Parser.scala index 0d573edce4..e8e7897ca4 100644 --- a/src/library/scala/util/parsing/json/Parser.scala +++ b/src/library/scala/util/parsing/json/Parser.scala @@ -15,6 +15,31 @@ import scala.util.parsing.combinator.syntactical._ import scala.util.parsing.combinator.lexical._ /** + * A marker class for the JSON result types. + * + * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> + */ +sealed abstract class JSONType + +/** + * 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 = "JSONObject(" + obj.map({ case (k,v) => k + " -> " + v }).mkString(", ") + ")" +} + +/** + * Represents a JSON Array (list). + * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> + */ +case class JSONArray (list : List[Any]) extends JSONType { + override def toString = "JSONArray(" + list.mkString(", ") + ")" +} + +/** + * The main JSON Parser. + * * @author Derek Chen-Becker <"java"+@+"chen-becker"+"."+"org"> */ class Parser extends StdTokenParsers with ImplicitConversions { @@ -39,8 +64,8 @@ class Parser extends StdTokenParsers with ImplicitConversions { // Define the grammar def root = jsonObj | jsonArray - def jsonObj = "{" ~> repsep(objEntry, ",") <~ "}" - def jsonArray = "[" ~> repsep(value, ",") <~ "]" + def jsonObj = "{" ~> repsep(objEntry, ",") <~ "}" ^^ { case vals : List[_] => JSONObject(Map(vals : _*)) } + def jsonArray = "[" ~> repsep(value, ",") <~ "]" ^^ { case vals : List[_] => JSONArray(vals) } def objEntry = stringVal ~ (":" ~> value) ^^ { case x ~ y => (x, y) } def value: Parser[Any] = (jsonObj | jsonArray | number | "true" ^^^ true | "false" ^^^ false | "null" ^^^ null | stringVal) def stringVal = accept("string", { case lexical.StringLit(n) => n} ) diff --git a/test/files/run/json.check b/test/files/run/json.check index a735624221..021214beaa 100644 --- a/test/files/run/json.check +++ b/test/files/run/json.check @@ -1,12 +1,17 @@ -Some(List((name,value))) -Some(List((name,va1ue))) -Some(List((name,List((name1,va1ue1), (name2,va1ue2))))) -Some(List((name,"))) -Some(List((age,0.0))) +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) -Some(List((firstName,John), (lastName,Smith), (address,List((streetAddress,21 2nd Street), (city,New York), (state,NY), (postalCode,10021.0))), (phoneNumbers,List(212 732-1234, 646 123-4567)))) +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)) -Some(List((fullname,Sean Kelly), (org,SK Consulting), (emailaddrs,List(List((type,work), (value,kelly@seankelly.biz)), List((type,home), (pref,1.0), (value,kelly@seankelly.tv)))), (telephones,List(List((type,work), (pref,1.0), (value,+1 214 555 1212)), List((type,fax), (value,+1 214 555 1213)), List((type,mobile), (value,+1 214 555 1214)))), (addresses,List(List((type,work), (format,us), (value,1234 Main StnSpringfield, TX 78080-1216)), List((type,home), (format,us), (value,5678 Main StnSpringfield, TX 78080-1316)))), (urls,List(List((type,work), (value,http://seankelly.biz/)), List((type,home), (value,http://seankelly.tv/)))))) +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/))) -Some(List((web-app,List((servlet,List(List((servlet-name,cofaxCDS), (servlet-class,org.cofax.cds.CDSServlet), (init-param,List((configGlossary:installationAt,Philadelphia, PA), (configGlossary:adminEmail,ksm@pobox.com), (configGlossary:poweredBy,Cofax), (configGlossary:poweredByIcon,/images/cofax.gif), (configGlossary:staticPath,/content/static), (templateProcessorClass,org.cofax.WysiwygTemplate), (templateLoaderClass,org.cofax.FilesTemplateLoader), (templatePath,templates), (templateOverridePath,), (defaultListTemplate,listTemplate.htm), (defaultFileTemplate,articleTemplate.htm), (useJSP,false), (jspListTemplate,listTemplate.jsp), (jspFileTemplate,articleTemplate.jsp), (cachePackageTagsTrack,200.0), (cachePackageTagsStore,200.0), (cachePackageTagsRefresh,60.0), (cacheTemplatesTrack,100.0), (cacheTemplatesStore,50.0), (cacheTemplatesRefresh,15.0), (cachePagesTrack,200.0), (cachePagesStore,100.0), (cachePagesRefresh,10.0), (cachePagesDirtyRead,10.0), (searchEngineListTemplate,forSearchEnginesList.htm), (searchEngineFileTemplate,forSearchEngines.htm), (searchEngineRobotsDb,WEB-INF/robots.db), (useDataStore,true), (dataStoreClass,org.cofax.SqlDataStore), (redirectionClass,org.cofax.SqlRedirection), (dataStoreName,cofax), (dataStoreDriver,com.microsoft.jdbc.sqlserver.SQLServerDriver), (dataStoreUrl,jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon), (dataStoreUser,sa), (dataStorePassword,dataStoreTestQuery), (dataStoreTestQuery,SET NOCOUNT ON;select test='test';), (dataStoreLogFile,/usr/local/tomcat/logs/datastore.log), (dataStoreInitConns,10.0), (dataStoreMaxConns,100.0), (dataStoreConnUsageLimit,100.0), (dataStoreLogLevel,debug), (maxUrlLength,500.0)))), List((servlet-name,cofaxEmail), (servlet-class,org.cofax.cds.EmailServlet), (init-param,List((mailHost,mail1), (mailHostOverride,mail2)))), List((servlet-name,cofaxAdmin), (servlet-class,org.cofax.cds.AdminServlet)), List((servlet-name,fileServlet), (servlet-class,org.cofax.cds.FileServlet)), List((servlet-name,cofaxTools), (servlet-class,org.cofax.cms.CofaxToolsServlet), (init-param,List((templatePath,toolstemplates/), (log,1.0), (logLocation,/usr/local/tomcat/logs/CofaxTools.log), (logMaxSize,), (dataLog,1.0), (dataLogLocation,/usr/local/tomcat/logs/dataLog.log), (dataLogMaxSize,), (removePageCache,/content/admin/remove?cache=pages&id=), (removeTemplateCache,/content/admin/remove?cache=templates&id=), (fileTransferFolder,/usr/local/tomcat/webapps/content/fileTransferFolder), (lookInContext,1.0), (adminGroupID,4.0), (betaServer,true)))))), (servlet-mapping,List((cofaxCDS,/), (cofaxEmail,/cofaxutil/aemail/*), (cofaxAdmin,/admin/*), (fileServlet,/static/*), (cofaxTools,/tools/*))), (taglib,List((taglib-uri,cofax.tld), (taglib-location,/WEB-INF/tlds/cofax.tld))))))) +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))) diff --git a/test/files/run/json.scala b/test/files/run/json.scala index ec0bad7ebe..0a141dc38c 100644 --- a/test/files/run/json.scala +++ b/test/files/run/json.scala @@ -1,14 +1,80 @@ import scala.util.parsing.json._ +import scala.collection.immutable.TreeMap object Test extends Application { - def printJSON(s: String) { - println(JSON parse s) + /* This method converts parsed JSON back into real JSON notation with objects in + * sorted-key order. Not required by the spec, but it allows us to to a stable + * toString comparison. */ + def jsonToString(in : Any) : String = in match { + case l : List[_] => "[" + l.map(jsonToString).mkString(", ") + "]" + case m : Map[String,_] => "{" + m.elements.toList + .sort({ (x,y) => x._1 < y._1 }) + .map({ case (k,v) => "\"" + k + "\": " + jsonToString(v) }) + .mkString(", ") + "}" + case s : String => "\"" + s + "\"" + case x => x.toString } - printJSON("{\"name\": \"value\"}") - printJSON("{\"name\": \"va1ue\"}") // ticket #136 - printJSON("{\"name\": { \"name1\": \"va1ue1\", \"name2\": \"va1ue2\" } }") + + def sortJSON(in : Any) : Any = in match { + case l : List[_] => l.map(sortJSON) + case m : Map[String,_] => TreeMap(m.mapElements(sortJSON).elements.toSeq : _*) + case x => x + } + + // For this one, just parsing should be considered a pass + def printJSON(given : String) { + JSON parseFull given match { + case None => println("Parse failed for \"%s\"".format(given)) + case Some(parsed) => println("Passed: " + sortJSON(parsed)) + } + } + + def printJSON(given : String, expected : Any) { + JSON parseFull given match { + case None => println("Parse failed for \"%s\"".format(given)) + case Some(parsed) => if (parsed == expected) { + println("Passed: " + parsed) + } else { + val eStr = sortJSON(expected).toString + val pStr = sortJSON(parsed).toString + + // 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) + case x => x + } + val reason = (" " * mismatchPosition) + "^" + println("Expected, got:\n %s\n %s (from \"%s\")\n %s".format(eStr, pStr, given, reason)) + } + } + } + + // 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"))) + + // Unicode escapes should be handled properly printJSON("{\"name\": \"\\u0022\"}") + + // The library should return a map for JSON objects (ticket #873) + printJSON("""{"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))) + + // The library should differentiate between empty maps and lists (ticket #3284) + printJSON("{}", Map()) + printJSON("[]", 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)) + + // Additional tests printJSON("{\"age\": 0}") + + println // from http://en.wikipedia.org/wiki/JSON @@ -27,8 +93,25 @@ object Test extends Application { "646 123-4567" ] }""" - //println(sample1) - printJSON(sample1) + + // Should be equivalent to: + val sample1Obj = Map( + "firstName" -> "John", + "lastName" -> "Smith", + "address" -> Map( + "streetAddress" -> "21 2nd Street", + "city" -> "New York", + "state" -> "NY", + "postalCode" -> 10021 + ), + "phoneNumbers"-> List( + "212 732-1234", + "646 123-4567" + ) + ) + + + printJSON(sample1, sample1Obj) println // from http://www.developer.com/lang/jscript/article.php/3596836 |