Among the new features of specs2 there will be JSON matchers to help those of you handling JSON data. When developing those matchers I used ScalaCheck to test some of the utility functions I was using. I want to show here that writing custom data generators with ScalaCheck is really easy and almost follows the grammar for the data.
Here's the code:
/**
* Generator of JSONType objects with a given tree depth
*/
import util.parsing.json._
import org.scalacheck._
import Gen._
trait JsonGen {
implicit def arbitraryJsonType: Arbitrary[JSONType] =
Arbitrary { sized(depth => jsonType(depth)) }
/** generate either a JSONArray or a JSONObject */
def jsonType(depth: Int): Gen[JSONType] = oneOf(jsonArray(depth), jsonObject(depth))
/** generate a JSONArray */
def jsonArray(depth: Int): Gen[JSONArray] = for {
n <- choose(1, 4)
vals <- values(n, depth)
} yield JSONArray(vals)
/** generate a JSONObject */
def jsonObject(depth: Int): Gen[JSONObject] = for {
n <- choose(1, 4)
ks <- keys(n)
vals <- values(n, depth)
} yield JSONObject(Map((ks zip vals):_*))
/** generate a list of keys to be used in the map of a JSONObject */
def keys(n: Int) = listOfN(n, oneOf("a", "b", "c"))
/**
* generate a list of values to be used in the map of a JSONObject or in the list
* of a JSONArray.
*/
def values(n: Int, depth: Int) = listOfN(n, value(depth))
/**
* generate a value to be used in the map of a JSONObject or in the list
* of a JSONArray.
*/
def value(depth: Int) =
if (depth == 0)
terminalType
else
oneOf(jsonType(depth - 1), terminalType)
/** generate a terminal value type */
def terminalType = oneOf(1, 2, "m", "n", "o")
}
/** import the members of that object to use the implicit arbitrary[JSONType] */
object JsonGen extends JsonGen
Two things to notice in the code above:
The generators are recursively defined, which makes sense because the JSON data format is recursive. For example a
jsonArray
contains values which can be aterminalType
or ajsonType
. ButjsonType
can itself be ajsonArray
- The top generator used in the definition of the
Arbitrary[JSONType]
is using a "sized" generator. This means that we can tweak the ScalaCheck parameters to use a specific "size" for our generated data. Here I've choosen to define "size" as being the depth of the generated JSON trees. This depth parameter is propagated to all generators until thevalue
generator. If depth is 0 when using that generator, this means that we reached the bottom of the Tree so we need a "terminal" value. Otherwise we generate another JSON object with a decremented depth.