Skip to content

Instantly share code, notes, and snippets.

@dustinevan
Last active November 20, 2020 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dustinevan/feadb5e956c1769ce47265a8be29741e to your computer and use it in GitHub Desktop.
Save dustinevan/feadb5e956c1769ce47265a8be29741e to your computer and use it in GitHub Desktop.
Kotlin API Clients using Request Options
/**
* This is a strategy I came up with to write HTTP API Clients in Kotlin. It has a couple of motivations:
* 1. Posting public documentation for your REST API is a nice thing to do, but requiring your customers to
* pay programmers to implement it is a bit of a bummer. If you write the HTTP API, you are going
* to be the proverbial 10x programmer when you write the API client. Your customers will thank you,
* especially if 'customers' means 'other programmers in the same organization'.
*
* 2. API versioning is a thing. Versioning the API Client automagically signals to users that
* the API is changing and how they need to adjust *via compilations errors!*
*
* 3. The Spring RestTemplate can be difficult to use
*/
/**
* A RequestOption is a function that modifies a request. They can be chained together--that's nice!
*/
typealias RequestOption = (Request) -> Request
/**
* Here is a list of RequestOptions transforming a request one at a time.
*/
fun List<RequestOption>.transform(request: Request): Request {
var req: Request = request
this.forEach { req = it(req) }
return req
}
/**
* Fuel's Request class doesn't make its query parameters available
* for modification, so this function 'transforms' the requests
* by making new ones
*/
fun parameters(vararg pairs: Pair<String, Any>): RequestOption {
fun addParameter(req: Request, param: Pair<String, Any>): Request {
var parameters = req.parameters.toMutableList()
parameters.add(param)
req.parameters = parameters
return req
}
return { req: Request ->
pairs.map { { request: Request -> addParameter(request, it) } }.transform(req)
}
}
/**
* If you like using Result in kotlin, go for it--imho exceptions are a benefit to writing in kotlin
* so this function is used to unwrap results and throw the exceptions.
*/
typealias ErrorHandler = (Request, Response, Result<Any, FuelError>) -> Throwable
val noOpErrorHandler: ErrorHandler = { _, _, result -> result.component2()!!.exception }
/**
* Here's an extension function to make it easy to transform a request
*/
fun Request.applyOptions(vararg options: List<RequestOption>) =
options.toList().flatten().transform(this)
/**
This is all the boilerplate you need to get going. Let's
do an example so the strategy is clear
Let's implement a REST API like this:
/api/weather/forecasts/10day?zip=77346&include=[humidity]
*/
class WeatherClient(
host: String,
token: String,
handler: ErrorHandler = noOpErrorHandler,
vararg defaultOptions: RequestOption // options you'd like on every request
) {
private val defaults: Array<RequestOption> =
arrayOf(
{ req: Request -> req.authentication().bearer(token) }, // let's add bearer auth to every request
*defaultOptions
)
val forecasts = ForecastAPI(host, handler, *defaults) // forecasts is a sub api of weather
}
fun zip(zip: Int): RequestOption = parameters(Pair("zip", zip))
fun include(vararg includes: Include): RequestOption =
parameters(Pair("include", includes.toList().map { it.pair }.toTypedArray()))
enum class Include(val value: String) {
HUMIDITY("humidity"),
SUNSET("sunset"),
SUNRISE("sunrise");
val pair = Pair("includes[]", value)
}
class ForecastAPI(
private val host: String,
private val handler: ErrorHandler,
vararg defaults: RequestOption
) {
fun tenDay(vararg options: RequestOption): TenDayResponse {
// See https://github.com/kittinunf/fuel
// Note how self documenting this code is. The whole path is right here for readability
val (request, response, result) = "https://$host/api/weather/forecasts/10day"
.httpGet()
.applyOptions(defaults, options)
.responseObject<TenDayResponse>()
return if (result is Result.Success) result.get()
else throw handler(request, response, result)
}
}
data class TenDayResponse(val forecast: String)
/**
The Payoff: Notice that the request is exposed to your API client user. If there's something they'd like to
do to modify the request, the API gives them that option. Also, if you're using Intellij, api paths will
show in the drop downs as you type! That's nice too!
*/
val weather = WeatherClient("weather", "token")
fun tenDayForecast() = {
val forecast = weather.forecasts.tenDay(include(Include.HUMIDITY), zip(77346), { request -> request.timeout(10) })
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment