You probably all know JSON - it's becoming the universal data exchange format, slowly but steadily replacing XML. In JavaScript, JSON is a proper first class citizen:
person = {
"name": "Joe Doe",
"age": 45,
"kids": ["Frank", "Marta", "Joan"]
};
person.age; // 45
person.kids[1]; // "Marta"
Sadly, it's not as easy in other languages. Scala does have a JSON parser in the standard library (scala.util.parsing.json.JSON), but it is terrible slow and the output is still not very nice.
val person_json = """{
"name": "Joe Doe",
"age": 45,
"kids": ["Frank", "Marta", "Joan"]
}"""
val person = scala.util.parsing.json.JSON.parseFull(person_json)
// returns "Joe Doe"
person match {
case Some(m: Map[String, Any]) => m("name") match {
case s: String => s
}
}
Luckily, it's easy to create a better and faster parser. I used the json-smart library to do the actual parsing (it's really fast!) and wrote a wrapper in Scala to make the results nicer to use.
A very important ingredient here is scala.Dynamic
which allows us to handle arbitrary method calls. That means we will be able to use JSON like this:
person.name.toString // "Joe Doe"
person.kids(1).toString // "Marta"
Strictly speaking, the toString
is not even necessary, as it's easy to define implicit conversions, but it makes it clearer what kind of result we expect.
Everything is built on two dynamic methods:
def selectDynamic(name: String): ScalaJSON = apply(name) // used for object.field
def applyDynamic(name: String)(arg: Any) = { // used for object.method(parm)
arg match {
case s: String => apply(name)(s)
case n: Int => apply(name)(n)
case u: Unit => apply(name) // sometimes called when field is accessed
}
}
The rest is really just a wrapper for the data structures returned by json-smart
, but it allows you to write really concise code:
package EasyJSON
import JSON._
object Test extends App {
print("Fetching recent data ...")
val json = io.Source.fromURL("http://liquid8002.untergrund.net/infinigag/").mkString
println(" done\n")
val tree = parseJSON(json)
println("next page: %d\n".format(tree.attributes.next.toInt))
println("=== Top %d images from 9gag ===".format(tree.images.length))
tree.images.foreach {
case image =>
println("%s: %s".format(image.title, image.image.thumb))
}
}
Check out the full implementation here (it also allows you to generate JSON):
package EasyJSON
import net.minidev.json.JSONValue
import net.minidev.json.JSONArray
import net.minidev.json.JSONObject
import scala.collection.JavaConversions._
object JSON {
def parseJSON(s: String) = new ScalaJSON(JSONValue.parse(s))
def makeJSON(a: Any): String = a match {
case m: Map[String, Any] => m.map {
case (name, content) => "\"" + name + "\":" + makeJSON(content)
}.mkString("{", ",", "}")
case l: List[Any] => l.map(makeJSON).mkString("[", ",", "]")
case l: java.util.List[Any] => l.map(makeJSON).mkString("[", ",", "]")
case s: String => "\"" + s + "\""
case i: Int => i.toString
}
implicit def ScalaJSONToString(s: ScalaJSON) = s.toString
implicit def ScalaJSONToInt(s: ScalaJSON) = s.toInt
implicit def ScalaJSONToDouble(s: ScalaJSON) = s.toDouble
}
case class JSONException extends Exception
class ScalaJSONIterator(i: java.util.Iterator[java.lang.Object]) extends Iterator[ScalaJSON] {
def hasNext = i.hasNext()
def next() = new ScalaJSON(i.next())
}
class ScalaJSON(o: java.lang.Object) extends Seq[ScalaJSON] with Dynamic {
override def toString: String = o.toString
def toInt: Int = o match {
case i: Integer => i
case _ => throw new JSONException
}
def toDouble: Double = o match {
case d: java.lang.Double => d
case f: java.lang.Float => f.toDouble
case _ => throw new JSONException
}
def apply(key: String): ScalaJSON = o match {
case m: JSONObject => new ScalaJSON(m.get(key))
case _ => throw new JSONException
}
def apply(idx: Int): ScalaJSON = o match {
case a: JSONArray => new ScalaJSON(a.get(idx))
case _ => throw new JSONException
}
def length: Int = o match {
case a: JSONArray => a.size()
case m: JSONObject => m.size()
case _ => throw new JSONException
}
def iterator: Iterator[ScalaJSON] = o match {
case a: JSONArray => new ScalaJSONIterator(a.iterator())
case _ => throw new JSONException
}
def selectDynamic(name: String): ScalaJSON = apply(name)
def applyDynamic(name: String)(arg: Any) = {
arg match {
case s: String => apply(name)(s)
case n: Int => apply(name)(n)
case u: Unit => apply(name)
}
}
}
Tags: programming, web