Implementing an Idiomatic Service DSL

With the solid foundation we created, it shouldn’t be too hard to implement a straightforward DSL for services; however, in many ways the design of the API is a harder problem, and much more a matter of taste than engineering. In this case our limitations can guide us. Since our limited space and scope prevent us from pursuing more elaborate options like macros and complex base traits, we can instead focus on a thoughtful arrangement of basic Scala patterns: case classes, method chaining, and implicits.

Designing a Service

We’ll again rely on Argonaut for serialization and deserialization. As you saw in the previous section, our Response class can hold any kind of value, but we need a good way to transform it into a String for our Server to transmit. We can do this with a little shorthand helper method that will make it simple for us to generate normal successful HTTP responses:

LibUVService/service.scala
 object​ ServiceHelpers {
 def​ OK[​T​](body​:​​T​, headers​:​​Map​[​String​,​String​] ​=​ Map.empty)
  (​implicit​ e​:​​EncodeJson​[​T​])​:​​Response​[​String​] ​=​ {
 val​ b ​=​ body.asJson.nospaces
  Response(200,​"OK"​,headers,b)
  }
 }

The Service class itself is also mostly straightforward: it’s initialized with a sequence of routes, and its dispatch method matches them by looking for a full prefix match on the incoming URL. Although this is a minimalistic design for a URL router, the implementation, by design, would allow a much more sophisticated design with named URL components, and so on, to be supplied. We’ll also provide a small helper method to start the Service and attach it to a Server:

LibUVService/service.scala
 case​ ​class​ Service(routes​:​​Seq​[​Route​] ​=​ Seq.empty)
  (​implicit​ ec​:​​ExecutionContext​) {
 def​ dispatch(req​:​​Request​[​String​])​:​​Route​ = {
 for​ (route ​<-​ routes) {
 if​ (req.method == route.method && req.url.startsWith(route.path)) {
  println(s​"matched route ($route)"​)
 return​ route
  }
  }
 throw​ ​new​ Exception(​"no match!"​)
  }
 
 def​ run(port​:​​Int​) ​=​ {
  Server.init(port, ​this​.dispatch)
  }

Now, all that’s left is the actual DSL.

Implementing a Router DSL

We’ll implement our router DSL by appending additional routes onto the Service’s (initially empty) list. Since the OK helper already does the hard work, the GET method handlers are very straightforward:

LibUVService/service.scala
 def​ get(path​:​​String​)(h​:​​Request​[​String​] ​=>​ Response[​String​])​:​​Service​ = {
 return​ Service(​this​.routes :+ SyncRoute(​"GET"​,path,h))
 }
 def​ getAsync(path​:​​String​)(h​:​​Request​[​String​] ​=>​ Future[​Response​[​String​]])​:
 Service​ = {
 return​ Service(​this​.routes :+ AsyncRoute(​"GET"​,path,h))
 }

The POST handlers are just a bit trickier, since they need to decode a request body:

LibUVService/service.scala
 def​ post[​I​,​O​](path​:​​String​)(h​:​​Request​[​I​] ​=>​ Response[​O​])
  (​implicit​ d​:​​DecodeJson​[​I​], e​:​​EncodeJson​[​O​])​:​​Service​ = {
 val​ handler ​=​ (r​:​​Request​[​String​]) ​=>​ {
 val​ parsedRequest ​=​ Parse.decodeOption[​I​](r.body) ​match​ {
 case​ Some(i) ​=>
  Request[​I​](r.method,r.url,r.headers,i)
  }
 val​ resp ​=​ h(parsedRequest)
  Response[​String​](resp.code, resp.description,
  resp.headers, resp.body.asJson.nospaces)
  }
 return​ Service(​this​.routes :+ SyncRoute(​"POST"​,path,handler))
 }
 
 def​ postAsync[​I​,​O​](path​:​​String​)(h​:​​Request​[​I​] ​=>​ Future[​Response​[​O​]])
  (​implicit​ d​:​​DecodeJson​[​I​], e​:​​EncodeJson​[​O​])​:​​Service​ = {
 val​ handler ​=​ (r​:​​Request​[​String​]) ​=>​ {
 val​ parsedRequest ​=​ Parse.decodeOption[​I​](r.body) ​match​ {
 case​ Some(i) ​=>
  Request[​I​](r.method,r.url,r.headers,i)
  }
  h(parsedRequest).map { resp ​=>
  Response[​String​](resp.code, resp.description,
  resp.headers, resp.body.asJson.nospaces)
  }
  }
 return​ Service(​this​.routes :+ AsyncRoute(​"POST"​,path,handler))
 }

With that, we can put together a true microservice composed of synchronous and asynchronous routes:

LibUVService/service.scala
 object​ Main {
 import​ ​LibUVConstants._​, LibUV.uv_run, ServiceHelpers.​_
 implicit​ ​val​ ec ​=​ EventLoop
 
 def​ main(args​:​​Array​[​String​])​:​​Unit​ = {
  Service()
  .getAsync(​"/async/"​) { r ​=>​ Future(OK(
  Map(​"asyncMessage"​ -> s​"got (async routed) request $r"​)
  ))}
  .get(​"/"​) { r ​=>​ OK(
  Map(​"message"​ -> s​"got (routed) request $r"​)
  )}
  .run(9999)
  uv_run(EventLoop.loop, UV_RUN_DEFAULT)
  println(​"done"​)
  }
 }

You can test this out with curl, or a browser, and get a sense of it’s flexibility and power. Argonaut’s JSON codecs, in particular, are flexible enough to handle lists, maps, case classes, and many other common data structures without boilerplate, which can make our services as compact as you might write in a scripting language, like Python or JavaScript, but with all the power and type-safety of Scala.