Hacker News new | ask | show | jobs
by sciolizer 4523 days ago
Here's a suggestion: design the SDK such that calling a method returns you everything you need to construct the REST call yourself: the verb, the url, the query params, the form params, and perhaps some headers.

    case class HttpCall[OutputType](
        verb: String,
        url: String,
        queryParams: List[(String,String)],
        formParams: List[(String,String)],
        resultType: Class[OutputType])

    trait Api {
      def getMyTasks(): HttpCall[List[Task]]
      def getTask(taskId: String): HttpCall[Task]
    }
The SDK can also provide a driver for making the http calls:

    trait Driver {
      def call[Out](httpCall: HttpCall[Out]): Out
    }
So now api invocations look like this:

    val myTasks: List[Task] = driver.call(api.getMyTasks())
    val aTask: Task = driver.call(api.getTask("foo"))
What's nice about this design is that the consumer can write their own driver if they don't like yours. Prefer asynchronous over synchronous? No problem!

    trait Driver {
      def call[Out](httpCall: HttpCall[Out]): Future[Out]
    }
Everything in the article except for grep-ability can be achieved with this kind of design.
2 comments

Great idea!

I can definitely see this as a workable solution. The only thought I have is that once in a while behavior like retry/idempotency support might have to be tweaked on a per-endpoint basis rather than from the general `call` method, and at that point things get a little less pretty.

In general I'd say that the ideas expressed in the article apply more to dynamic languages where you see much more of the convention of SDKs just giving you back hashes instead of strongly-typed models representing foreign resources. In something like Scala, the barrier of having to implement every model yourself might be an extra motivator for just defaulting to the SDK.

Barf. Forcing boilerplate into the code of EVERY caller of your library "just in case" somebody might want to do something slightly different.