Welcome to the sixth blog of 52-technologies-in-2016 blog series. In this blog, we will learn how to write Scala REST API client for Medium's REST API using OkHttp library. REST APIs have become a standard method of communication between two devices over a network. Most applications expose their REST API that developers can use to get work with an application programmatically. For example, if I have to build a realtime opinion mining application then I can use Twitter or Facebook REST APIs to get hold of their data and build my application. To work with an application REST APIs, you either can write your own client or you can use one of the language specific client provided by the application. Last few weeks, I have started using Medium for posting non-technical blogs. Medium is a blog publishing platform created by Twitter co-founder Evan Williams. Evan Williams is the same guy who earlier created Blogger, which was bought by Google in 2003.
Medium exposed their REST API to the external world last year. The API is simple and allows you to do operations like submitting a post, getting details of the authenticated user, getting publications for a user, etc. You can read about Medium API documentation in their Github repository. Medium officially provides REST API clients for Node.js, Python, and Go programming languages. I couldn't find Scala client for Medium REST API so I decided to write my own client using OkHttp.
OkHttp is an open source Java HTTP client library focussed on efficiency. It is written by folks at Square. It supports SPDY, HTTP/2, and WebSocket protocols.
OkHttp API is very easy to use. You just have to add its dependency to your classpath and then you can start using it to build your clients.
According to OkHttp documentation,
OkHttp is an HTTP client that’s efficient by default:
- HTTP/2 support allows all requests to the same host to share a socket.
- Connection pooling reduces request latency (if HTTP/2 isn’t available).
- Transparent GZIP shrinks download sizes.
- Response caching avoids the network completely for repeat requests.
I know you must be thinking why I am using a Java library to build a Scala REST API client. Like most Scala developers, I thought of using a Scala library instead. But, as I started looking into which Scala library should I use I didn't find any single winner. If you search for "Scala REST client", you will land up on this StackOverFlow question. It suggests four libraries Dispatch, Scalaz http, spray-client, Play WS. Let's discuss why I didn't used them one by one.
-
Dispatch: It is a Scala wrapper around Ning's Async Http Client. The project doesn't look very active with last commit on May 30, 2015. The travis-ci build is also broken so I am not sure if this project is actively maintained.
-
Scalaz http: Scalaz is an extension to the core Scala library for functional programming. They used to have an HTTP client. They dropped http module from Scalaz in version 7.
-
spray-client: It provides high-level HTTP client functionality by adding another logic layer on top of the relatively basic spray-can HTTP Client APIs. spray-client depends on many other spray projects and Akka. I didn't wanted to use a library that depends on so many other libraries.
-
Play WS: Play WS is part of the Scala's Play web framework. It can used in standalone mode but it also depends on many other libraries. It also looked very heavy weight for something simple. So, I decided not to use it as well.
My reasons for going with OkHttp are:
- It has only one dependency Okio. Okio is a library that complements java.io and java.nio to make it much easier to access, store, and process your data.
- It has very good testing support. It provides scriptable web server for testing HTTP client. This makes it easy to test whether your client is doing the right thing without depending on the network.
- OkHttp is one of the few libraries that is designed up front for efficiency.
- Stable and actively developed by Square. Last commit was 15 hours ago.
- API is very simple and intuitive to use. It comes with good defaults and works like a charm.
Although, OkHttp is a Java library but it works great with Scala. I know it might not be the Scala way but sometimes we have to become pragmatic and choose the right tool for the job. There is also an OkHttp Scala wrapper called Communicator that one can use.
The code for today’s application is available on github: medium-scala-client. In this blog, I will only cover couple of REST endpoints. You can view the full source of medium-scala-sdk here.
Start by creating a new directory medium-scala-client
at a convenient location on your filesystem. This directory will house the source code of our client.
$ mkdir medium-scala-client
Create a new file build.sbt
inside the medium-scala-client
directory. build.sbt
is the sbt build script.
If you are new to sbt, then please refer to my earlier post on it.
Populate build.sbt
with following contents.
name := "medium-scala-client"
version := "1.0"
description := "Scala client for Medium.com REST API"
scalaVersion := "2.11.7"
libraryDependencies += "com.squareup.okhttp3" % "okhttp" % "3.0.1"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.2"
libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.6" % "test"
libraryDependencies += "com.squareup.okhttp3" % "mockwebserver" % "3.0.1" % "test"
In the build script shown above, you can see that we have only added two compile time dependencies -- okhttp
and spray-json
. spray-json is a lightweight, clean, and efficient library to work with JSON in Scala. It has no dependencies. We will use it to convert our domain objects into JSON and vice-versa. scalatest
and mockwebserver
are added for testing.
Create a project layout for your Scala source and test files.
$ mkdir -p src/main/{scala,resources}
$ mkdir -p src/test/scala
Let's start with implementing the REST endpoint to get details of an authenticated user. To get the details of a user, we have to make an HTTP GET request.
GET https://api.medium.com/v1/me
We will start with writing a test. Create a new package medium
inside the src/test/scala
. After creating the package, create a Scala class MediumClientSpec
. Populate the MediumClientSpec
with following contents.
package medium
import okhttp3.mockwebserver.MockWebServer
import org.scalatest.{BeforeAndAfterEach, FunSpec, Matchers}
class MediumClientSpec extends FunSpec with Matchers with BeforeAndAfterEach{
var server: MockWebServer = _
override protected def beforeEach(): Unit = {
server = new MockWebServer()
}
override protected def afterEach(): Unit = {
server.shutdown()
}
}
The code shown above does the following:
- We created a new class
MediumClientSpec
that extendedFunSpec
,Matchers
, andBeforeAndAfterEach
traits. These are part ofscalatest
library. - We override two methods of
BeforeAndAfterEach
trait.beforeEach
will make sure thatMockWebServer
instance is created before each test case is executed.MockWebServer
is a scriptable web server. You can configure it to return mock responses for your requests. It works very similarly to any mocking framework. You first set your expectations, then run the application code, and finally verify that expected requests were made. afterEach
will make sure that server is shutdown after each test.
Add the following test case to the MediumClientSpec
. This code should be added after the afterEach
method.
describe("MediumClientSpec") {
it("should get details of an authenticated user") {
val json =
"""
|{
| "data": {
| "id": "123",
| "username": "shekhargulati",
| "name": "Shekhar Gulati",
| "url": "https://medium.com/@shekhargulati",
| "imageUrl": "https://cdn-images-1.medium.com/fit/c/200/200/1*pC-eYQUV-iP2Y10_LgGvwA.jpeg"
| }
|}
""".stripMargin
server.enqueue(new MockResponse()
.setBody(json)
.setHeader("Content-Type", "application/json")
.setHeader("charset", "utf-8"))
server.start()
val medium = new MediumClient("test_client_id", "test_client_secret", Some("access_token")) {
override val baseApiUrl = server.url("/v1/me")
}
val user = medium.getUser
user should have(
'id ("123"),
'username ("shekhargulati"),
'name ("Shekhar Gulati"),
'url ("https://medium.com/@shekhargulati"),
'imageUrl ("https://cdn-images-1.medium.com/fit/c/200/200/1*pC-eYQUV-iP2Y10_LgGvwA.jpeg")
)
}
}
Let's understand the code show above:
- We created a json that will be returned by
MockWebServer
when GET request is made tohttps://api.medium.com/v1/me
. - Then, we set up the server with a mock response. We set the body to the json created in step 1. Also, we added HTTP headers that will be passed in the response.
- Next, we started the mock web server so that it can accept test requests.
- Then, we created an instance of MediumClient(that we will create later in the blog). We have to set the URL returned by our server in the client so that it makes requests to the mock server instead of hitting the actual Medium API. This is the reason we have overridden
baseApiUrl
value ofMediumClient
. - Finally, we called the
getUser
method ofMediumClient
and asserted its response.
Now that we have written our test case we should start working on the implementation of MediumClient. Create a new package medium
inside src/main/scala
. Then, create a new Scala class MediumClient
inside the medium
package.
package medium
class MediumClient(clientId: String, clientSecret: String, var accessToken: Option[String] = None)
object MediumClient {
def apply(clientId: String, clientSecret: String): MediumClient = new MediumClient(clientId, clientSecret)
def apply(clientId: String, clientSecret: String, accessToken: String): MediumClient = new MediumClient(clientId, clientSecret, Some(accessToken))
}
case class MediumException(message: String, cause: Throwable = null) extends RuntimeException(message, cause)
The code shown above does the following:
- We created a new Scala class
MediumClient
. The primary constructor of MediumClient takes three arguments --clientId
,clientSecret
, andaccessToken
. The clientId and clientSecret are created for you when you create new Medium application http://medium.com/me/applications. Using the clientId and clientSecret, users can generateaccessToken
. You have to passaccessToken
in each request to the Medium API. - Then, we created a companion object to the
MediumClient
. It provides factory methods to easily constructMediumClient
instances. MediumException
is a runtime exception that we will throw when client will not be able to process user requests.
Create an instance of OkHttpClient
inside the MediumClient
class as shown below. OkHttpClient
is used to send HTTP requests and read HTTP responses. When you create the OkHttpClient
instance using the default constructor then an OkHttpClient
instance is created using the default values. You can also create an instance configured using other values by using the OkHttpClient.Builder
API. We also created another value baseApiUrl
of type okhttp3.HttpUrl
. This will store the base URL of the Medium API i.e. https://api.medium.com
.
import okhttp3.{HttpUrl, OkHttpClient}
class MediumClient(clientId: String, clientSecret: String, var accessToken: Option[String] = None) {
val client = new OkHttpClient()
val baseApiUrl: HttpUrl = new HttpUrl.Builder()
.scheme("https")
.host("api.medium.com")
.build()
}
Now, we will write the getUser
method that will make an HTTP GET request to the Medium API to fetch the user details. User is determined using the accessToken
. If access token is not set then it will throw MediumException
.
def getUser: User = accessToken match {
case Some(at) => ???
case _ => throw new MediumException("Please set access token")
}
The Scala syntax
???
lets you write a not yet implemented method. This allows you to write code that compiles. But, if you run this code, then it will thrown an exception.
The code shown above needs User
to compile. Create a new Scala object domainObjects
. The domainObjects.scala
will house all our domain objects like User
, Post
, etc. Create a case class for User
inside it as shown below.
package medium
object domainObjects {
case class User(id: String, username: String, name: String, url: String, imageUrl: String)
}
As shown above, we created a User case class with five fields inside the domainObjects
Scala object.
After creating the domainObjects
Scala object, add its import in the MediumClient
so that code can compile.
import domainObjects._
class MediumClient(clientId: String, clientSecret: String, var accessToken: Option[String] = None)
Now, let's write code to replace ???
with actual implementation inside the getUser
method.
def getUser: User = accessToken match {
case Some(at) =>
val request = new Request.Builder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Accept-Charset", "utf-8")
.header("Authorization", s"Bearer $at")
.url(baseApiUrl.resolve("/v1/me"))
.get()
.build()
makeRequest[User](request)
case _ => throw new MediumException("Please set access token")
}
private def makeRequest[T](request: Request)(implicit p: JsonReader[T]): T = ???
In the code shown above:
- We created request using the OkHttp
Request.Builder
API. We set the required headers in the request and set url of the request to/v1/me
.HttpUrl.resolve
method resolves the url against the baseApiUrl. So, the full url will becomehttps://api.medium.com/v1/me
. OkHttp understands which HTTP method to use by looking at the request. As you can see above, we called theget
method of the request builder. This constructs an immutableokhttp3.Request
object. - Once request is created, we passed the request to
makeRequest
method. This method will process any request be it GET or POST or DELETE and return the domain object.
Now, we will implement makeRequest
method. makeRequest
method makes use of OkHttpClient
instance to create a new Call
. To make the HTTP call, we first called newCall
method on OkHttpClient
instance. The newCall
returns okhttp3.Call
object. OkHttp uses Call
to model the task of satisfying your request through however many intermediate requests and responses are necessary. Calls can be executed in synchronous or asynchronous manner. In the code shown below, we called the execute
method to make a synchronous HTTP GET call.
private def makeRequest[T](request: Request)(implicit p: JsonReader[T]): T= {
val response = client.newCall(request).execute()
val responseJson = response.body().string()
println(s"Received response $responseJson")
???
}
If you run the test method now, it will render the json
response we have set in the test.
Received response
{
"data": {
"id": "123",
"username": "shekhargulati",
"name": "Shekhar Gulati",
"url": "https://medium.com/@shekhargulati",
"imageUrl": "https://cdn-images-1.medium.com/fit/c/200/200/1*pC-eYQUV-iP2Y10_LgGvwA.jpeg"
}
}
Now, let's take a look at the last bit of code required to convert json into User
object. To convert json into User object, we will make use of spray-json
library.
To use spray-json
, we have to first add few imports so that relevant elements are added in the scope of our MediumClient
.
import spray.json._
import DefaultJsonProtocol._
After adding the imports, you can convert the json string into User object as shown below.
private def makeRequest[T](request: Request)(implicit p: JsonReader[T]): T= {
val response = client.newCall(request).execute()
val responseJson = response.body().string()
println(s"Received response $responseJson")
response match {
case r if r.isSuccessful =>
val jsValue: JsValue = responseJson.parseJson
jsValue.asJsObject.getFields("data").headOption match {
case Some(data) => data.convertTo[T]
case _ => throw new MediumException(s"Received unexpected JSON response $responseJson")
}
case _ => throw new MediumException(s"Received HTTP error response code ${response.code()}")
}
}
The code shown above will not compile as you have to bring implicit values in scope that provide JsonFormat[User]
instances for User.
Create a new object MediumApiProtocol
that will define a JsonFormat
to convert User
into JSON.
package medium
import medium.domainObjects.User
import spray.json.DefaultJsonProtocol
object MediumApiProtocol extends DefaultJsonProtocol{
implicit val userFormat = jsonFormat5(User)
}
Now, code will compile and test case will pass.
Let's now implement method that will create a post on Medium. To create a post, we have to use HTTP POST method as we are creating a resource on the server. Let's write a test method, that will test the post creation.
it("should publish a new post") {
val responsJson =
"""
|{
| "data": {
| "id": "e6f36a",
| "title": "Liverpool FC",
| "authorId": "5303d74c64f66366f00cb9b2a94f3251bf5",
| "tags": ["football", "sport", "Liverpool"],
| "url": "https://medium.com/@majelbstoat/liverpool-fc-e6f36a",
| "canonicalUrl": "http://jamietalbot.com/posts/liverpool-fc",
| "publishStatus": "public",
| "publishedAt": 1442286338435,
| "license": "all-rights-reserved",
| "licenseUrl": "https://medium.com/policy/9db0094a1e0f"
| }
|}
""".stripMargin
server.enqueue(new MockResponse()
.setBody(responsJson)
.setHeader("Content-Type", "application/json")
.setHeader("charset", "utf-8"))
server.start()
val medium = new MediumClient("test_client_id", "test_client_secret", Some("access_token")) {
override val baseApiUrl = server.url("/v1/users/123/posts")
}
val content =
"""
|# Hello World
|Hello how are you?
|## What's up today?
|Writing REST client for Medium API
""".stripMargin
val post = medium.createPost("123", PostRequest("Liverpool FC", "html", content))
post.id should be("e6f36a")
}
Next, add PostRequest
and Post
case classes to domainObjects.scala
.
package medium
object domainObjects {
case class User(id: String, username: String, name: String, url: String, imageUrl: String)
case class PostRequest(title: String, contentFormat: String, content: String, tags: Array[String] = Array(), canonicalUrl: Option[String] = None, publishStatus: String = "public", license: String = "all-rights-reserved")
case class Post(id: String, publicationId: Option[String] = None, title: String, authorId: String, tags: Array[String], url: String, canonicalUrl: String, publishStatus: String, publishedAt: Long, license: String, licenseUrl: String)
}
Write the JSON formatter in MediumApiProtocol
as shown below.
package medium
import medium.domainObjects._
import spray.json._
object MediumApiProtocol extends DefaultJsonProtocol{
implicit val userFormat = jsonFormat5(User)
implicit val postRequestFormat = jsonFormat7(PostRequest)
implicit val postFormat = jsonFormat11(Post)
}
Now, we will write createPost
that will create a Medium post.
def createPost(authorId: String, postRequest: PostRequest): Post = accessToken match {
case Some(at) =>
val httpUrl = baseApiUrl.resolve(s"/v1/users/$authorId/posts")
val request = new Request.Builder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Accept-Charset", "utf-8")
.header("Authorization", s"Bearer $at")
.url(httpUrl)
.post(RequestBody.create(MediaType.parse("application/json"), postRequest.toJson.prettyPrint))
.build()
makeRequest[Post](request)
case _ => throw new MediumException("Please set access token")
}
Now, compile the code and run the test case. Both test cases will pass now.
This week we learnt how to write REST API using OkHttp library. We covered how to make HTTP GET and POST requests using OkHttp. OkHttp supports all HTTP methods like head, delete, put, etc. You can also use OKHttp to make asynchronous calls. You can refer to OkHttp documentation for more details.
That's all for this week. Please provide your valuable feedback by adding a comment to shekhargulati#8.