At the end of part 3 we had a Domain Specific Language (DLS) that could neatly express a complete REST use case. It fully described the requests and their expected responses.
val Jason: Person = ???
val personJson = Json.stringify(Jason)
val EmptyList = List[Person]()
RequestBuilder() url "http://api.rest.org/person" apply { implicit rb =>
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is EmptyList)
val id = POST body personJson asserting (statusCode is Status.Created) returning (header("X-Person-Id"))
GET / id asserting (statusCode is Status.OK, jsonBodyAs[Person] is Jason)
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is Seq(Jason))
DELETE / id asserting (statusCode is Status.OK)
GET / id asserting (statusCode is Status.NotFound)
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is EmptyList)
}
Unfortunately how it specifies common request properties and the statements that depend on it is not as fluid as the rest of the syntax.
RequestBuilder() url "http://api.rest.org/person" apply { implicit rb =>
// ...
}
There are a number of issues with this expression:
- The intent, supplying a common url, is hidden between
RequestBuilder()
andapply { implicit rb =>
. - Ideally the current
RequestBuilder
instance would be passed into the expression implicitly instead of requiring theRequestBuilder()
call. - The
apply
method suggests the common properties are applied to the nested expressions. However in Scala theapply
method has special meaning and is normally called with bracket syntax. This is confusing to novice and experienced Scala programmers alike. Hereapply
must be expressed explicitly because of the lower precedence of infix expressions. A DSL should not have to explain its self like this. It should be obvious and follow the idioms of its host language. - The syntax is not very discoverable. How would a user figure out that pairing a
RequestBuilder()
with anapply
method call would enable common properties to be reused.
Scala places some constraints on infix expressions:
- They should be of the form
<subject> <verb> <object>
, this expression can return a new subject resulting in<subject> <verb1> <object1> <verb2> <object2>
etc. So expressions must have an old number of terms, in our example the code block is the last term. - Expressions that do not follow this pattern must be terminated or wrapped with parentheses.
- The expression must also be bootstrapped with a subject, in our case the current
RequestBuilder
object. The call toRequestBuilder()
does this but it is very verbose. Some DSLs introduce a special symbol in this case but that can obscure the intent even more.
Can this expression be revised to highlight its intent, work with Scala norms and be more discoverable? How can we work around the constraints of infix expressions?
using(_ url "http://api.rest.org/person") { implicit rb =>
// ...
}
- The
using
method looks like a standard control structure, which encapsulates the feature in a single expression. It has two clear sections:- common request configuration in parentheses.
- and a code block for expressions that reuse this configuration.
- This simpler structure makes the intent clearer to someone reading the code for the first time.
- It also provides a single place to document the feature making it more discoverable, and easier to write for the first time.
- The single underscore
_
represents the currentRequestBuilder
this is a little subtle but follows the standard Scala convention for anonymous functions. It is shorter thanRequestBuilder()
and is also more idiomatic than a special DSL specific symbol. - The
apply
method does not have to be explicitly specified because the parentheses around the expression enable the Scala compiler to infer it.
So how is the using
method implemented?
def using(config: RequestBuilder => RequestBuilder)
(process: RequestBuilder => Unit)
(implicit builder: RequestBuilder): Unit = {
process(config(builder))
}
The using
method takes three parameter lists each with a single parameter. Breaking the parameters into their own list enables the method to be used like a control structure, where the function in the second parameter is specified as a code block.
The config
parameter is just a function that takes a current RequestBuilder
and returns the updated RequestBuilder
required by the enclosing block. This function is typically applied between ( ... )
and uses the _
syntax to generate the function. The process
parameter is another function taking a RequestBuilder
and returning Unit
. This is typically applied with { implicit rb => ... }
, my making the rb
parameter implicit the using
method can be nested. The final builder
parameter is the currently configured RequestBuilder
, this is passed in implicitly.
The implementation is very simple, generate updated RequestBuilder
by calling the config
function with implicit builder
. Then pass the result to the process
function, where the updated RequestBuilder
is used.
Now the full use case looks like:
val Jason: Person = ???
val personJson = Json.stringify(Jason)
val EmptyList = List[Person]()
using(_ url "http://api.rest.org/person") { implicit rb =>
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is EmptyList)
val id = POST body personJson asserting (statusCode is Status.Created) returning (header("X-Person-Id"))
GET / id asserting (statusCode is Status.OK, jsonBodyAs[Person] is Jason)
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is Seq(Jason))
DELETE / id asserting (statusCode is Status.OK)
GET / id asserting (statusCode is Status.NotFound)
GET asserting (statusCode is Status.OK, jsonBodyAsList[Person] is EmptyList)
}
Next I want to examine how assertions are implemented and whether they can be integrated with ScalaTest. I also have some opinions on how best to test DSLs. But both of these will have to wait for a subsequent post.