[UPDATE] I pushed the code as SJersey to Github. For usage examples check out the test code here [/UPDATE]
The are many awesome REST frameworks out there, for Java and for Scala as well. Most of the concepts do cover the server side. Important I know, but where are the smart client side APIs/DSLs? There are some and I must confess, I dislike them all. So this is a try to create another one. One that I dislike to a lesser extend.
I like Jersey, a production ready REST client and server framework for REST. The server implementation follows the JAX-RS standard and has an awesome functionality. And since I know this framework best, it is my choice for the client side as well.
I wrote a little one-source-file Jersey wrapper for Scala. The REST services are operated by Neo4j Server, a NoSQL GraphDB. My specs tests are running against it. All objects have to be marshaled and unmarshaled in and to JSON. Jersey uses JAXB for that.
Unfortunately, this wrapper is far away from complete… Anyway, here is my proposal:
We have, at least, the REST methods GET, PUT, POST and DELETE. The following should be supported (not limited to):
- there always is a base URI for all communication. In case of Neo4j it is
http://localhost:7474/db/data/
. But we should be able to overwrite it or to append a path for specific REST method calls - GET should return the expected JSON object, without this annoying
classOf
or.class
stuff - PUT and POST should be able, additionally to GET, to take a request entity parameter to post f.e. a new node.
- DELETE should support a Client Response object to check wether the deletion was successful
First of all, we have to extend from two traits SimpleWebResourceProvider
, that supports the creation of Jersey Client
and WebResource
instances and trait Rest
. Rest is the trait for all Jersey wrapping functionality.
Class MyRestServiceClient
has to implement method baseUriAsString
to provide the base URI within this class and to overwrite mediaType
to set the media type from default to JSON.
class MyRestServiceClient extends Rest with SimpleWebResourceProvider { // base location of Neo4j server instance def baseUriAsString = "http://localhost:7474/db/data/" // all subsequent REST calls should use JSON notation override val mediaType = Some(MediaType.APPLICATION_JSON) . . . }
All REST method calls have to be enclosed in method rest(...) {}
. rest can be parametrized with additional header parameter and a basePath that is appended to the global base URI. This is valid for all REST method calls within method rest.
rest(header = ("MyHeaderParameter1", "1") :: ("MyHeaderParameter2", "2") :: Nil) {} rest(basePath = "node/1/") {} rest{}
The following GetRoot
is one example of a JAXB object declaration. GetRoot is returned by a GET call.
@XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement class GetRoot { var index:String = _ var node:String = _ var reference_node:String = _ }
The declared JAXB objects can now be used in all REST methods. The following example returns Neo4j’s root entity and an index entity. Types are declared inside Scala’s [], so there is no classOf operator needed (BTW, you can not write val root:GetRoot = "".GET
. Sad that I don’t know why…)
rest { implicit s => val root = "".GET[GetRoot] val index = "/index".GET[GetIndex] }
For POST we have to provide a request entity. It is PathRequest
in this case, a JAXB object, that provides the parameter for the traversal of node 3 and depth 4. The POST call returns a JSONArray
with the traversal data (unfortunately, JAXB thrown an exception using the usual JAXB objects). The “operator” <=
is used to define the request entity.
rest{ implicit s => val path = "node/3/traverse/path".POST[JSONArray] <= PathRequest(order = "depth first", max_depth = 4, uniqueness = "node path") }
Same for PUT. A PUT without response in this case. Request entity is MatrixNodeProperties
, part of the common Neo4j graph example. The properties are set and read here.
rest(basePath = "node/1/") { implicit s => "properties".PUT <= MatrixNodeProperties(name = "Thomas Anderson Neo", profession = "Hacker") val properties = "properties".GET[MatrixNodeProperties] }
The next more complex example shows how nodes can be created and deleted with a DELETE method call. Since the Location header of the POST response contains the whole URI of the created node, I had to provide a unary ! operator to overwrite all global paths and use the given one as absolute path.
(I hope that the comments are sufficient for explanation)
rest{ implicit s => // defining note names and profession val nodes = ("Mr. Andersson", "Hacker") :: ("Morpheus", "Hacker") :: ("Trinity", "Hacker") :: ("Cypher", "Hacker") :: ("Agent Smith", "Program") :: ("The Architect", "Whatever") :: Nil // for all notes val locations = for (node <- nodes; // create node cr = "node".POST[ClientResponse] <= MatrixNodeProperties(name = node._1, profession = node._2) // if creation was successful use yield if (cr.getStatus == ClientResponse.Status.CREATED.getStatusCode) // yield all created locations ) yield cr.getLocation // and remove them for (location <- locations) { // the unary ! is used to sign a absolute path (location here) val cr = (!location.toString).DELETE[ClientResponse] // no exception and No Content means successful cr.getStatus mustEqual ClientResponse.Status.NO_CONTENT.getStatusCode } }
I hope you like it? What do you think?
Ran across this project the other day, not sure if it is of interest to you. https://github.com/codahale/jersey-scala