how to write a Dispatch plugin

in

Dispatch has been the de facto library to get to the Internet from Scala. To keep in step with the recent move towards non-blocking IO, @n8han rewrote the library as Reboot based on Async Http Client. This became Dispatch 0.9. Then Dispatch 0.10 came out to replace its own Promise type with the standarized SIP-14 Future.

As with Dispatch Classic, Reboot lets you write plugins to wrap around web APIs. In this post we'll port a plugin from Classic to explore how it works.

working on your own twitter bot?

repatch-twitter

To avoid naming collision with other potential plugins, I'm going to call mine repatch-twitter, instead of dispatch-twitter.

sbt

First thing to do is to set up sbt:

repatch-twitter/
  +- project/
  |    +- build.properties
  |    +- build.scala
  +- core/
       +- src/
            +- main/
                 +- scala/
                      +- requests.scala
                      +- ...

The content of build.properties:

sbt.version=0.12.3

The content of build.scala:

import sbt._
 
object Builds extends Build {
  import Keys._
  lazy val dispatchVersion = SettingKey[String]("x-dispatch-version")
 
  lazy val buildSettings = Defaults.defaultSettings ++ Seq(
    dispatchVersion := "0.10.0",
    version <<= dispatchVersion { dv => "dispatch" + dv + "_0.1.0-SNAPSHOT" },
    organization := "com.eed3si9n",
    scalaVersion := "2.10.1",
    libraryDependencies <++= (dispatchVersion) { (dv) => Seq(
      "net.databinder.dispatch" %% "dispatch-core" % dv,
      "net.databinder.dispatch" %% "dispatch-json4s-native" % dv
    )},
    libraryDependencies <+= (scalaVersion) {
      case "2.9.3" =>  "org.specs2" %% "specs2" % "1.12.4.1" % "test"
      case _ => "org.specs2" %% "specs2" % "1.15-SNAPSHOT" % "test"
    },
    crossScalaVersions := Seq("2.10.1"),
    resolvers += "sonatype-public" at "https://oss.sonatype.org/content/repositories/public"
  )
  lazy val coreSettings = buildSettings ++ Seq(
    name := "repatch-twitter-core"
  )
 
  lazy val root = Project("root", file("."),
    settings = buildSettings ++ Seq(name := "repatch-twitter")) aggregate(core)
  lazy val core = Project("core", file("core"), settings = coreSettings)
}

Since Dispatch 0.10.0 uses SIP-14 Future, it's currently available only for Scala 2.10 and 2.9.3.

general idea

There are mainly two parts to what a Dispatch plugin would provide. First part is Req building classes. You can provide classes or functions that represent API end points and various parameters. Another useful thing is to abstract authentication method. The eventual job is to create Req object that gets passed into an Http instance.

The second part is the response handler. You can either provide series of parsing functions, or define a case class that represents the results. You can omit this part, and let the app developer deal with the responses directly too.

request wrapper

Let's try wrapping GET search/tweets.

First, we'll define a Method by extending Req => Req. It will accept a Req object from authentication wrapper and return a Req:

package repatch.twitter.request
 
import dispatch._
import org.json4s._
 
trait Method extends (Req => Req) {
  def complete: Req => Req
  def apply(req: Req): Req = complete(req)
}

Now we'll define a case class that represents the API end point:

// https://api.twitter.com/1.1/search/tweets.json
case class Search(params: Map[String, String]) extends Method {
  def complete = _ / "search" / "tweets.json" <<? params
}
case object Search {
  def apply(q: String): Search = Search(Map("q" -> q))
}

authentication wrapper

We need a few more steps to use this. First, during the runtime, each API call needs to be signed by an OAuth access token.

package repatch.twitter.request
 
import dispatch._
import oauth._
import com.ning.http.client.oauth._
 
/** AbstractClient is a function to wrap API operations */
trait AbstractClient extends (Method => Req) {
  def hostName = "api.twitter.com"
  def host = :/(hostName).secure / "1.1"
  def apply(method: Method): Req = method(host)  
}
 
// ConsumerKey(key: String, secret: String) 
// RequestToken(key: String, token: String) 
case class OAuthClient(consumer: ConsumerKey, token: RequestToken) extends AbstractClient {
  override def apply(method: Method): Req = method(host) sign(consumer, token)
}

We'll create an OAuthClient once, which extends Method => Req and pass in a Method into it to generate a Req. We'll get back to this later.

Second thing we have to worry about is how we create ConsumerKey and RequestToken. The consumer key can be issued for your application by going to My applications, and creating one.

The application page should show Consumer key and secret under OAuth settings, and Access token and secret under Your access token. For now we can use this to test the search/tweets API.

Start Scala REPL from sbt shell after switching to core project:

scala> import dispatch._, Defaults._
import dispatch._
import Defaults._
 
scala> import com.ning.http.client.oauth._
import com.ning.http.client.oauth._
 
scala> import repatch.twitter.request._
import repatch.twitter.request._
 
scala> val consumer = new ConsumerKey("abcd", "secret")
consumer: com.ning.http.client.oauth.ConsumerKey = {Consumer key, key="abcd", secret="secret"}
 
scala> val accessToken = new RequestToken("xyz", "secret")
accessToken: com.ning.http.client.oauth.RequestToken = { key="xyz", secret="secret"}
 
scala> val client = OAuthClient(consumer, accessToken)
client: repatch.twitter.request.OAuthClient = <function1>
 
scala> val http = new Http
http: dispatch.Http = Http(com.ning.http.client.AsyncHttpClient@52f1234c)
 
scala> http(client(Search("#scala")) OK as.json4s.Json)
res0: dispatch.Future[org.json4s.JValue] = scala.concurrent.impl.Promise$DefaultPromise@346fbd9a
 
scala> res0()
res1: org.json4s.JValue = 
JObject(List((statuses,JArray(List(JObject(List((metadata,JObject(List((result_type,JString(recent)), (iso_language_code,JString(es))))), (created_at,JString(Mon May 06 00:46:14 +0000 2013)), (id,JInt(331208247845462016)), (id_str,JString(331208247845462016)), (text,JString(Emanuel Goette, alias Crespo: Migration Manager for #Scala http://t.co/bzr028uEwe)), (source,JString(<a href="http://twitter.com/tweetbutton" rel="nofollow">Tweet Button</a>)), (truncated,JBool(false)), (in_reply_to_status_id,JNull), (in_reply_to_status_id_str,JNull), (in_reply_to_user_id,JNull), (in_reply_to_user_id_str,JNull), (in_reply_to_screen_name,JNull), (user,JObject(List((id,JInt(121934271)), (id_str,JString(121934271)), (name,JString(Emanuel)), (screen_name,JString(emanuelpeg)), (...

Looks like we got some tweets! But remember currently it's using access token on behalf of you, so we need a way of acquiring an access token for the app user.

Note: When using sbt console with Dispatch 0.10.0 causes CPU to get pegged at 100% when you quit out of the console. @n8han thinks it's particular to sbt console. The only workaround for now is to quit sbt.

OAuth exchange

This is where OAuthExchange comes in:

trait TwitterEndpoints extends SomeEndpoints {
  def requestToken: String = "https://api.twitter.com/oauth/request_token"
  def accessToken: String = "https://api.twitter.com/oauth/access_token"
  def authorize: String = "https://api.twitter.com/oauth/authorize"
}
 
case class OAuthExchange(http: HttpExecutor, consumer: ConsumerKey, callback: String) extends
  SomeHttp with SomeConsumer with TwitterEndpoints with SomeCallback with Exchange {
}

The basic idea is to create a request token, let the user authorize the request token using the browser, and then retrieve the access token using the verification code. I'm going to assume we're developing a desktop application, which requires out-of-band authorization:

scala> val exchange = OAuthExchange(http, consumer, "oob")
exchange: repatch.twitter.request.OAuthExchange = OAuthExchange(Http(com.ning.http.client.AsyncHttpClient@4293aa50),{Consumer key, key="abcd", secret="secret"},oob)
 
scala> val x = exchange.fetchRequestToken
x: scala.concurrent.Future[Either[String,com.ning.http.client.oauth.RequestToken]] = scala.concurrent.impl.Promise$DefaultPromise@45d45cb6
 
scala> val reqToken = x() match {
     |   case Right(t) => t
     |   case Left(s)  => sys.error(s)
     | }
reqToken: com.ning.http.client.oauth.RequestToken = { key="rxyz", secret="rsecret"}
 
scala> val authorizeUrl = exchange.signedAuthorize(reqToken)
authorizeUrl: String = https://api.twitter.com/oauth/authorize?oauth_token=rxyz&oauth_signature=xxxxx%3D

Now make the app user open the browser and open the authorizeUrl to retrieve the PIN.

scala> val x2 = exchange.fetchAccessToken(reqToken, "1234567")
x2: scala.concurrent.Future[Either[String,com.ning.http.client.oauth.RequestToken]] = scala.concurrent.impl.Promise$DefaultPromise@5ae1b5e6
 
scala> val accessToken = x2() match {
     |   case Right(t) => t
     |   case Left(s)  => sys.error(s)
     | }
accessToken: com.ning.http.client.oauth.RequestToken = { key="xyz", secret="secret2"}
 
scala> val client = OAuthClient(consumer, accessToken)
client: repatch.twitter.request.OAuthClient = <function1>

You can save the access token somewhere safe to reuse it next time. To avoid dealing with tokens in the examples, let's define ProperitesClient that creates OAuthClient from a properties file:

object ProperitesClient {
  def apply(props: Properties): OAuthClient = {
    val consumer = new ConsumerKey(props getProperty "repatch.twitter.consumerKey",
      props getProperty "repatch.twitter.consumerKeySecret")
    val token = new RequestToken(props getProperty "repatch.twitter.accessToken",
      props getProperty "repatch.twitter.accessTokenSecret")
    OAuthClient(consumer, token)
  }
  def apply(file: File): OAuthClient = {
    val props = new Properties()
    props load new FileInputStream(file)
    apply(props)
  }
}

Now we can write both consumer key and the access token in a properities file as follows:

repatch.twitter.consumerKey=abc
repatch.twitter.consumerKeySecret=secret
repatch.twitter.accessToken=xyz
repatch.twitter.accessTokenSecret=secret2

Here's how to load these up:

scala> import dispatch._, Defaults._
import dispatch._
import Defaults._
 
scala> import repatch.twitter.request._
import repatch.twitter.request._
 
scala> val prop = new java.io.File(System.getProperty("user.home"), ".foo.properties")
prop: java.io.File = /Users/you/.foo.properties
 
scala> val client = PropertiesClient(prop)
client: repatch.twitter.request.OAuthClient = <function1>
 
scala> val http = new Http

Enough about OAuth. Let's talk about Dispatch.

Now that we can sign requests on behalf of the app user, this could be considered a minimally useful plugin.

providing query parameter support

Looking at GET search/tweets, there are number of parameters we can pass in, so we can provide them as methods on Search class.

import java.util.Calendar
import java.text.SimpleDateFormat
 
trait Show[A] {
  def shows(a: A): String
}
object Show {
  def showA[A]: Show[A] = new Show[A] {
    def shows(a: A): String = a.toString 
  }
  implicit val stringShow  = showA[String]
  implicit val intShow     = showA[Int]
  implicit val bigIntShow  = showA[BigInt]
  implicit val booleanShow = showA[Boolean]
  private val yyyyMmDd = new SimpleDateFormat("yyyy-MM-dd")
  implicit val calendarShow: Show[Calendar] = new Show[Calendar] {
    def shows(a: Calendar): String = yyyyMmDd.format(a.getTime)
  }
}

The above is a Show typeclass, which lets us control how to print each types as String. Except for Calendar I'm just using the default toString.

// https://api.twitter.com/1.1/search/tweets.json
case class Search(params: Map[String, String]) extends Method with Param[Search] {
  def complete = _ / "search" / "tweets.json" <<? params
 
  def param[A: Show](key: String)(value: A): Search =
    copy(params = params + (key -> implicitly[Show[A]].shows(value)))
  private def geocode0(unit: String) = (lat: Double, lon: Double, r: Double) =>
    param[String]("geocode")(List(lat, lon, r).mkString(",") + unit)
  val geocode_mi = geocode0("mi")
  val geocode  = geocode0("km")
  val lang     = 'lang[String]
  val locale   = 'locale[String]
  /**  mixed, recent, popular */
  val result_type = 'result_type[String]
  val count    = 'count[Int]
  val until    = 'until[Calendar]
  val since_id = 'since_id[BigInt]
  val max_id   = 'max_id[BigInt]
  val include_entities = 'include_entities[Boolean]
  val callback = 'callback[String]
}
case object Search {
  def apply(q: String): Search = Search(Map("q" -> q))
}
 
trait Param[R] {
  val params: Map[String, String]
  def param[A: Show](key: String)(value: A): R
  implicit class SymOp(sym: Symbol) {
    def apply[A: Show]: A => R = param(sym.name)_
  }
}

This is inspired by dispatch-twitter's param, but it's typesafe and more concise. I'm injecting apply method to Symbol, which then partially applies its name to param. As the result val lang = 'lang[String] defines String => Search point-free style.

Here's how we can use this to query two tweets about "#scala" in the 10 mile radius from New York City:

scala> val x = http(client(Search("#scala").geocode_mi(40.7142, -74.0064, 10).count(2)) OK as.json4s.Json)
x: dispatch.Future[org.json4s.JValue] = scala.concurrent.impl.Promise$DefaultPromise@3252d2de
 
scala> val json = x()
json: org.json4s.JValue = 
JObject(List((statuses,JArray(List(JObject(List((metadata,JObject(List((result_type,JString(recent)), (iso_language_code,JString(en))))), (created_at,JString(Sun May 05 06:27:50 +0000 2013)), (id,JInt(330931826879234049)), (id_str,JString(330931826879234049)), (text,JString(Rocking the contravariance. Hard. #nerd #scala)), (source,JString(web)), (truncated,JBool(false)), (in_reply_to_status_id,JNull), (in_reply_to_status_id_str,JNull), (in_reply_to_user_id,JNull), (in_reply_to_user_id_str,JNull), (in_reply_to_screen_name,JNull), (user,JObject(List((id,JInt(716931690)), (id_str,JString(716931690)), (name,JString(Alex Lo)), (screen_name,JString(alexlo03)), (location,JString(New York, New York)), (description,JString(what?)), (url,JString(http://t.co/jMjRuK7h19))...

providing response parsing support

The next step is to provide parsing support for the returned json. There are two schools of thought on this issue. One way is to provide a set of functions that parses one field at a time. The other way is to provide converters that parse some of the known fields and create case classes. Dispatch allows both methods to be implemented side-by-side.

The benefit of field parser approach is the flexibility. Since it won't be tied to specific set of fields, it can seamlessly catch up to additional fields if API started returning more fields. The downside is that the app developer needs to specify the all fields she wants explicitly, which is more verbose.

The benefit of the case class converter approach is the convenience. Just ask for the case class, and parsing is taken care of. The downside is that it won't be able to keep up with API changes if it added more fields, and that there's 22 fields limitation. I'm aware of SI-7296.

A hybrid approach is to provide both, so the app developer can try the case classes that might cover the typical uses, and fallback to field parser if it's not enough.

field parser

Again, we'll start by defining a basic typeclass.

package repatch.twitter.response
 
import dispatch._
import org.json4s._
import java.util.{Calendar, Locale}
import java.text.SimpleDateFormat
 
trait ReadJs[A] {
  import ReadJs.=>?
  val readJs: JValue =>? A
}
object ReadJs {
  type =>?[-A, +B] = PartialFunction[A, B]
  def readJs[A](pf: JValue =>? A): ReadJs[A] = new ReadJs[A] {
    val readJs = pf
  }
  implicit val listRead: ReadJs[List[JValue]] = readJs { case JArray(v) => v }
  implicit val objectRead: ReadJs[JObject]    = readJs { case JObject(v) => JObject(v) }
  implicit val bigIntRead: ReadJs[BigInt]     = readJs { case JInt(v) => v }
  implicit val intRead: ReadJs[Int]           = readJs { case JInt(v) => v.toInt }
  implicit val stringRead: ReadJs[String]     = readJs { case JString(v) => v }
  implicit val boolRead: ReadJs[Boolean]      = readJs { case JBool(v) => v }
  private val twitterFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss ZZZZZ yyyy", Locale.ENGLISH)
  twitterFormat.setLenient(true)
  implicit val calendarRead: ReadJs[Calendar] =
    readJs { case JString(v) =>
      val date = twitterFormat.parse(v)
      val c = new GregorianCalendar
      c.setTime(date)
      c
    }
}

This lets me abstract json parsing. Using this as a building block, we can again enrich Symbol to inject methods into it:

object Search extends Parse {
  val statuses        = 'statuses.![List[JValue]]
  val search_metadata = 'search_metadata.![JObject]
}
 
trait Parse {
  def parse[A: ReadJs](js: JValue): Option[A] =
    implicitly[ReadJs[A]].readJs.lift(js)
  def parse_![A: ReadJs](js: JValue): A = parse(js).get
  def parseField[A: ReadJs](key: String)(js: JValue): Option[A] = parse[A](js \ key)
  def parseField_![A: ReadJs](key: String)(js: JValue): A = parseField(key)(js).get
  implicit class SymOp(sym: Symbol) {
    def apply[A: ReadJs]: JValue => Option[A] = parseField[A](sym.name)_
    def ![A: ReadJs]: JValue => A = parseField_![A](sym.name)_
  }
}

Note that we are now in response package, so this is different Search object than before. In the above statuses is a function JValue => List[JValue] again defined in point-free style. To actually parse the content of a tweet, we need to go one step further and look at Tweets.

/** https://dev.twitter.com/docs/platform-objects/tweets 
 */
object Tweet extends Parse {
  val contributors   = 'contributors[List[JValue]]
  val coordinates    = 'coordinates[JObject]
  val created_at     = 'created_at.![Calendar]
  val current_user_retweet = 'current_user_retweet[JObject]
  val entities       = 'entities.![JObject]
  val favorite_count = 'favorite_count[Int]
  val favorited      = 'favorited[Boolean]
  val filtere_level  = 'filtere_level[String]
  val id             = 'id.![BigInt]
  val id_str         = 'id_str.![String]
  val in_reply_to_screen_name   = 'in_reply_to_screen_name[String]
  val in_reply_to_status_id     = 'in_reply_to_status_id[BigInt]
  val in_reply_to_status_id_str = 'in_reply_to_status_id_str[String]
  val in_reply_to_user_id       = 'in_reply_to_user_id[BigInt]
  val in_reply_to_user_id_str   = 'in_reply_to_user_id_str[String]
  val lang           = 'lang[String]
  val place          = 'place[JObject]
  val possibly_sensitive = 'possibly_sensitive[Boolean]
  val scopes         = 'scopes[JObject]
  val source         = 'source.![String]
  val retweet_count  = 'retweet_count.![Int]
  val retweeted      = 'retweeted.![Boolean]
  val text           = 'text.![String]
  val truncated      = 'truncated.![Boolean]
  val user           = 'user[JObject]
  val withheld_copyright    = 'withheld_copyright[Boolean]
  val withheld_in_countries = 'withheld_in_countries[List[JValue]]
  val withheld_scope        = 'withheld_scope[String]
}

Here's how we can use the field parsers:

scala> {
         import repatch.twitter.response.Search._
         import repatch.twitter.response.Tweet._
         for {
           t <- statuses(json)
         } yield(id_str(t), text(t))
       }
res0: List[(String, String)] = List((330931826879234049,Rocking the contravariance. Hard. #nerd #scala), (330877539461500928,RT @mhamrah: Excellent article on structuring distributed systems with #rabbitmq. Thanks @heroku Scaling Out with #Scala and #Akka http://t…))

Next, let's look into the case class converters.

providing case class converter

We start with picking out useful fields and ordering them in some way that makes sense.

case class Tweet(
  id: BigInt,
  text: String,
  created_at: Calendar,
  user: Option[JObject],
  favorite_count: Option[Int],
  favorited: Option[Boolean],
  retweet_count: Int,
  retweeted: Boolean,
  truncated: Boolean,
  source: String,
  lang: Option[String],
  coordinates: Option[JObject],
  entities: JObject,
  in_reply_to_status_id: Option[BigInt],
  in_reply_to_user_id: Option[BigInt]
)

That should satisfy majority of the use cases. Next, implement an apply method that parses JValue into the case class.

/** https://dev.twitter.com/docs/platform-objects/tweets 
 */
object Tweet extends Parse {
  val contributors   = 'contributors[List[JValue]]
  ....
 
  def apply(js: JValue): Tweet = Tweet(
    id = id(js),
    text = text(js),
    created_at = created_at(js),
    user = user(js),
    favorite_count = favorite_count(js),
    favorited = favorited(js),
    retweet_count = retweet_count(js),
    retweeted = retweeted(js),
    truncated = truncated(js),
    source = source(js),
    lang = lang(js),
    coordinates = coordinates(js),
    entities = entities(js),
    in_reply_to_status_id = in_reply_to_status_id(js),
    in_reply_to_user_id = in_reply_to_user_id(js)   
  )
}

It's tedious to repeat the field name twice, but it's better than making sure you get the order right.

We should make a case class for Search too:

case class Search(
  statuses: List[Tweet],
  search_metadata: JObject
)
 
/** https://dev.twitter.com/docs/api/1.1/get/search/tweets
 */
object Search extends Parse {
  val statuses        = 'statuses.![List[JValue]]
  val search_metadata = 'search_metadata.![JObject]
 
  def apply(js: JValue): Search = Search(
    statuses = statuses(js) map {Tweet(_)},
    search_metadata = search_metadata(js)
  )
}

Next is a bit weird. We're going to define a package object for dispatch.as.repatch.twitter.response package. This is because the package name as is used by displatch.as, which is where you're suppose to hang your response converters. We probably could get away with something shorter, I'm just going to append repatch.twitter.response.

package dispatch.as.repatch.twitter
 
package object response {
  import com.ning.http.client.Response    
  import repatch.twitter.{response => r}
  import dispatch.as.json4s.Json
 
  val Search: Response => r.Search = Json andThen r.Search.apply
}

This will make sense soon. Remember the search call? Now we can convert the result strait to a case class:

scala> val x2 = http(client(Search("#scala").geocode_mi(40.7142, -74.0064, 10).count(2)) OK
         as.repatch.twitter.response.Search)
x2: dispatch.Future[repatch.twitter.response.Search] = scala.concurrent.impl.Promise$DefaultPromise@6bc9806d
 
scala> val search = x2()
search: repatch.twitter.response.Search = Search(List(Tweet(330931826879234049,Rocking the contravariance. Hard. #nerd #scala,java.util.GregorianCalendar[time=1367735270000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/New_York",offset=-18000000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=America/New_York,offset=-18000000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2013,MONTH=4,WEEK_OF_YEAR=19,WEEK_OF_MONTH=2,DAY_OF_MONTH=5,DAY_OF_YEAR=125,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=1...

As you can see, the usage code is simpler with this one. This is pretty much the idea.

Users

Tweet object embeds User object, so let's create fields support and case classes for it too.

case class User(
  id: BigInt,
  screen_name: String,
  created_at: Calendar,
  name: String,
  `protected`: Boolean,
  description: Option[String],
  location: Option[String],
  time_zone: Option[String],
  url: Option[String],
  verified: Boolean,
  statuses_count: Int,
  favourites_count: Int,
  followers_count: Int,
  friends_count: Int,
  default_profile: Boolean,
  default_profile_image: Boolean,
  profile_image_url: String,
  profile_image_url_https: String,
  lang: Option[String],
  entities: JObject
)
 
/** https://dev.twitter.com/docs/platform-objects/users
 */
object User extends Parse with CommonField {
  val contributors_enabled  = 'contributors_enabled.![Boolean]
  val default_profile       = 'default_profile.![Boolean]
  val default_profile_image = 'default_profile_image.![Boolean]
  val description           = 'description[String]
  val favourites_count      = 'favourites_count.![Int]
  ....
 
  def apply(js: JValue): User = User(
    id = id(js),
    screen_name = screen_name(js),
    created_at = created_at(js),
    name = name(js),
    `protected` = `protected`(js),
    ....
  )
}
 
trait CommonField { self: Parse =>
  val id                    = 'id.![BigInt]
  val id_str                = 'id_str.![String]
  val created_at            = 'created_at.![Calendar]
  val entities              = 'entities.![JObject]
  val lang                  = 'lang[String]
  val withheld_copyright    = 'withheld_copyright[Boolean]
  val withheld_in_countries = 'withheld_in_countries[List[JValue]]
  val withheld_scope        = 'withheld_scope[String]
}

Replace Tweet's user field with User.

case class Tweet(
  id: BigInt,
  text: String,
  created_at: Calendar,
  user: Option[User],
  ....
)

Statuses

Now that we have Tweet and User, we should be close to getting normal statuses. See GET statuses/home_timeline.

object Status {
  /** See https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline.
   * Wraps https://api.twitter.com/1.1/statuses/home_timeline.json
   */ 
  def home_timeline: HomeTimeline = HomeTimeline()
  case class HomeTimeline(params: Map[String, String] = Map()) extends Method
      with Param[HomeTimeline] with CommonParam[HomeTimeline] {
    def complete = _ / "statuses" / "home_timeline.json" <<? params
 
    def param[A: Show](key: String)(value: A): HomeTimeline =
      copy(params = params + (key -> implicitly[Show[A]].shows(value)))
    val trim_user       = 'trim_user[Boolean]
    val exclude_replies = 'exclude_replies[Boolean]
    val contributor_details = 'contributor_details[Boolean]
    val include_entities = 'include_entities[Boolean]
  }
}
 
trait CommonParam[R] { self: Param[R] =>
  val count           = 'count[Int]
  val since_id        = 'since_id[BigInt]
  val max_id          = 'max_id[BigInt]
}

Let's try using this:

scala> val x = http(client(Status.home_timeline.count(2)) OK as.json4s.Json)
x: dispatch.Future[org.json4s.JValue] = scala.concurrent.impl.Promise$DefaultPromise@42d2d985
 
scala> x()
res1: org.json4s.JValue = 
JArray(List(JObject(List((created_at,JString(Tue May 07 08:06:09 +0000 2013)), (id,JInt(...

Since this returns an array of tweets, we can convert the result into List[Tweet]. The following goes into response package:

object Tweets extends Parse {
  def apply(js: JValue): List[Tweet] =
    parse_![List[JValue]](js) map { x => Tweet(x) }
}

And here are the converters:

package object response {
  ....
  val Tweets: Response => List[response.Tweet] = Json andThen response.Tweets.apply
  val Statuses: Response => List[response.Tweet] = Tweets
  val Tweet: Response => response.Tweet = Json andThen response.Tweet.apply
  val Status: Response => response.Tweet = Tweet
}

We got the timelines.

scala> val x = http(client(Status.home_timeline) OK as.repatch.twitter.response.Tweets)
x: dispatch.Future[repatch.twitter.response.Statuses] = scala.concurrent.impl.Promise$DefaultPromise@41ad625a
 
scala> x()
res0: List[repatch.twitter.response.Tweet] = 
List(Tweet(331691122629951489,Partially applying a function that has an implicit parameter http://t.co/CwWQAkkBAN,....

Sending tweets

Sending tweets is also simple. See POST statuses/update.

object Status {
  ...
 
  /** See https://dev.twitter.com/docs/api/1.1/post/statuses/update
   */
  def update(status: String): Update = Update(Map("status" -> status))
  case class Update(params: Map[String, String]) extends Method with Param[Update] {
    def complete = _ / "statuses" / "update.json" << params
 
    def param[A: Show](key: String)(value: A): Update =
      copy(params = params + (key -> implicitly[Show[A]].shows(value)))
    val in_reply_to_status_id = 'in_reply_to_status_id[BigInt]
    val lat             = 'lat[Double]
    val `long`          = 'long[Double]
    val place_id        = 'place_id[String]
    val display_coordinates = 'display_coordinates[Boolean]
    val trim_user       = 'trim_user[Boolean]
  }
}

Here's how to use it.

scala> val x = http(client(Status.update("testing from REPL")) OK as.json4s.Json)
x: dispatch.Future[org.json4s.JValue] = scala.concurrent.impl.Promise$DefaultPromise@65056d18
 
scala> x()
res4: org.json4s.JValue = JObject(List((user,JObject(List((time_zone,JString(Eastern Time (US & Canada))), (created_at,JString(Fri Dec 22 15:19:02 +0000 2006)), (default_profile_image,JBool(false)), (name,JString(eugene yokota))...

A friend replied to the above tweet.

Let's send him a reply, but this time getting the result as a Tweet.

scala> val timeline = http(client(Status.home_timeline) OK as.repatch.twitter.response.Tweets)
timeline: dispatch.Future[List[repatch.twitter.response.Tweet]] = scala.concurrent.impl.Promise$DefaultPromise@515b96e5
 
scala> val to = timeline() filter { t => t.text contains "@eed3si9n" } head
to: repatch.twitter.response.Tweet = Tweet(331722441514708992,@eed3si9n working on your own twitter bot?...
 
scala> val x2 = http(client(Status.update("@LordOmlette wrapping Twitter API for an async http lib")
         in_reply_to_status_id to.id ) OK as.repatch.twitter.response.Tweet)
x2: dispatch.Future[repatch.twitter.response.Tweet] = scala.concurrent.impl.Promise$DefaultPromise@1d57cae4
 
scala> x2()
res8: repatch.twitter.response.Tweet = Tweet(331776040668102656,@LordOmlette wrapping Twitter API for an async http lib...

summary

Dispatch Reboot by design encourages separating concerns, namely request building, and response processing. For repatch-twitter, we defined request.Search case class for building GET search/tweets requests, and response.Search for processing the response. Instead of defining various symbolic operators for each resource type, new Dispatch handles them by converters under defined under dispatch.as.* package. Because the response processing is separated from request building, the app developer can always opt to use just the request objects and fall back to raw json parsing.

In general, it's important to consider the user experience of the plugin from the app developer's point of view. The usage code should be readable even if the person is not familiar with Dispatch or your plugin.

On the other hand, the we have more freedom in the implementation. That doesn't mean it doesn't have to be readable, but you can be more daring. For example, we implemented mini DSL by injecting apply[A] method to Symbol, and abstracted reading and writing using Scalaz-like typeclass. This could be cryptic to read for the unacquainted, but easy to maintain.

The source written in this post is available on github as eed3si9n/repatch-twitter.