Autowire and BooPickle

Web clients communicate with the server most commonly with Ajax which is a quite loosely defined collection of techniques. Most notable JavaScript libraries like JQuery provide higher level access to the low level protocols exposed by the browser. Scala.js provides a nice Ajax wrapper in dom.extensions.Ajax (or dom.ext.Ajax in scalajs-dom 0.8+) but it's still quite tedious to serialize/deserialize objects and take care of all the dirty little details.

But fear not, there is no need to do all that yourself since our friend Li Haoyi (lihaoyi) has created and published a great library called Autowire. Combined with my very own BooPickle library you can easily handle client-server communication. Note that BooPickle uses binary serialization format, so if you'd prefer a JSON format, consider using uPickle. As the SPA tutorial used to use uPickle for serialization, you can browse the repository history to see the relevant code here and here.

To build your own client-server communication pathway all you need to do is to define a single object on the client side and another on the server side.

import boopickle.Default._

// client side
object AjaxClient extends autowire.Client[ByteBuffer, Pickler, Pickler] {
  override def doCall(req: Request): Future[ByteBuffer] = {
    dom.ext.Ajax.post(
      url = "/api/" + req.path.mkString("/"),
      data = Pickle.intoBytes(req.args),
      responseType = "arraybuffer",
      headers = Map("Content-Type" -> "application/octet-stream")
    ).map(r => TypedArrayBuffer.wrap(r.response.asInstanceOf[ArrayBuffer]))
  }

  override def read[Result: Pickler](p: ByteBuffer) = Unpickle[Result].fromBytes(p)
  override def write[Result: Pickler](r: Result) = Pickle.intoBytes(r)
}

The only variable specific to your application is the URL you want to use to call the server. Otherwise everything else it automatically generated for you through the magic of macros. The server side is even simpler, just letting Autowire know that you want to use BooPickle for serialization.

import boopickle.Default._

// server side
object Router extends autowire.Server[ByteBuffer, Pickler, Pickler] {
  override def read[R: Pickler](p: ByteBuffer) = Unpickle[R].fromBytes(p)
  override def write[R: Pickler](r: R) = Pickle.intoBytes(r)
}

Now that you have the AjaxClient set up, calling the server is as simple as

import scala.concurrent.ExecutionContext.Implicits.global
import boopickle.Default._
import autowire._

AjaxClient[Api].getTodos().call().foreach { todos =>
  println(s"Got some things to do $todos")
}

Note that you need those three imports to access the Autowire/BooPickle magic and to provide an execution context for the futures.

The Api is just a simple trait shared between the client and server.

trait Api {
  // message of the day
  def motd(name:String) : String

  // get Todo items
  def getTodos() : Seq[TodoItem]

  // update a Todo
  def updateTodo(item: TodoItem): Seq[TodoItem]

  // delete a Todo
  def deleteTodo(itemId: String): Seq[TodoItem]
}

Please check out BooPickle documentation on what it can and cannot serialize. You might need to use something else if your data is complicated. Case classes, base collections and basic data types are a safe bet.

So how does this work on the server side?

results matching ""

    No results matching ""