We implement RESTful Web Service on Scala

Last week on Habré there were as many as two articles on the implementation of RESTful web-services in Java. Well, let's not lag behind and write our own version on Scala, with monads and applicative functors. Experienced Scala developers are unlikely to find anything new in this article, and Django fans will generally say that they have this functionality out of the box, but I hope that Java developers and just curious people will be interested to read.

Training


We take the task from the previous article as the basis , but we will try to solve it so that the solution code fits on the screen. At least a 40-inch and fifth font. In the end, in the XXI century it should be possible to solve simple tasks without megabytes of xml-configs and dozens of abstract factories.

For those who do not want to follow the links I’ll clarify: we are going to implement the simplest RESTful service for accessing the customer database. Of the necessary functionality - the creation and deletion of objects in the database, as well as pagination of a list of all clients with the ability to sort by different fields.

As the bricks from which we will build a house, we take:
  • Scala is not even a brick, but rather a foundation,
  • Unfiltered is a great library for handling HTTP requests,
  • Squeryl - a library for database queries,
  • Jackson is a library for working with JSON, originally written for Java, but with a bang it copes with Scala types,
  • Scalaz is a library that allows you to write different funny characters in the code like ↦, ↦, or ∃, and at the same time implements useful abstractions such as applicative functors, monoids, semigroups, and Kleisley arrows. True, I have not yet had to use the latter, but this is most likely due to the fact that I have not yet reached the required degree of functional enlightenment.

In the course of the article, I will try to give enough explanations so that the code can be understood by people who are not familiar with Scala, but I do not promise that I will succeed.


To battle


Data model

First we need to decide on a data model. Squeryl allows you to specify the model in the form of a regular class, and in order not to write too much, we will use the same class for subsequent serialization in JSON.

@JsonIgnoreProperties(Array("_isPersisted"))
case class Customer(id:        String,
                    firstName: String,
                    lastName:  String,
                    email:     Option[String],
                    birthday:  Option[Date]) extends KeyedEntity[String]

Fields of type Option[_] correspond to nullable columns of the database. Such fields can take two kinds of values: Some(value) if there is a value, and None if it is not. Use Option allows you to minimize the chances of occurrence NullPointerException and is a common practice in functional programming languages ​​(especially those in which null there is no NullPointerException concept null at all).

Annotation @JsonIgnoreProperties excludes certain fields from JSON serialization. In this case, we had to exclude the field _isPersisted that Squeryl added.

Initializing a Database Schema

Those who have worked with JDBC know that the first thing to do is to initialize the database driver class. Let's not deviate from this practice:

Class.forName("org.h2.Driver")

SessionFactory.concreteFactory =
  Some(() => Session.create(DriverManager.getConnection("jdbc:h2:test", "sa", ""), new H2Adapter))

In the first line, we load the JDBC driver, and in the second we tell the Squeryl library which connection factory to use. As a database we use light and fast H2 .

Now the turn of the scheme has come:

object DB extends Schema {
  val customer = table[Customer]
}

transaction { allCatch opt DB.create }

First, we indicate that our database contains one table that corresponds to the class Customer , and then we execute DDL commands to create this table. In real life, using automatic table creation is usually problematic, but for a quick demonstration it is very convenient. If the tables already exist in the database, it will DB.create throw an exception, which we, thanks to allCatch opt , will successfully ignore.

JSON serialization and deserialization

To start, let's initialize the JSON parser so that it can work with data types accepted in Scala:

val mapper = new ObjectMapper().withModule(DefaultScalaModule)

Now we define two functions for turning JSON strings into objects:

def parseCustomerJson(json: String): Option[Customer] =
  allCatch opt mapper.readValue(json, classOf[Customer])

def readCustomer(req: HttpRequest[_], id: => String): Option[Customer] =
  parseCustomerJson(Body.string(req)) map (_.copy(id = id))

The function parseCustomerJson is actually parsing JSON. Due to the use of allCatch opt exceptions that have arisen in the process of parsing, will be caught and as a result we get None . The second function,, readCustomer is directly related to the processing of the HTTP request - it reads the request body, turns it into an object of type Customer and sets the field id to the specified value.

It is worth noting that it was not necessary to specify the type of the return value in both functions: the compiler had enough data to deduce the type without the help of a programmer, but the explicitly specified type sometimes facilitates human understanding of the code.

The reverse process is the transformation of an object Customer (or list) List[Customer] ) into the body of the HTTP response - also does not represent complexity:

case class ResponseJson(o: Any) extends ComposeResponse(
  ContentType("application/json") ~> ResponseString(mapper.writeValueAsString(o)))

In the future, we will simply return objects of the type ResponseJson , and the Unfiltered framework will take care of turning it into the correct HTTP response.

Another small touch is the generation of new customer identifiers. The easiest, though not always the most convenient way, is to use the UUID:

def nextId = UUID.randomUUID().toString

HTTP request processing

Now that most of the preparatory work has been done, we can proceed directly to the implementation of the web service. I will not go into details of the Unfiltered library device, I’ll just say that the simplest way to use it looks like this:

val service = cycle.Planify {
  case /* шаблон запроса */ => /* код, генерирующий ответ */
}

Our service will have two entry points: /customer and /customer/[id] . Let's start with the second one:

case req@Path(Seg("customer" :: id :: Nil)) => req match {
  case GET(_) => transaction { DB.customer.lookup(id) cata(ResponseJson, NotFound) }
  case PUT(_) => transaction { readCustomer(req, id) ∘ DB.customer.update cata(_ => Ok, BadRequest) }
  case DELETE(_) => transaction { DB.customer.delete(id); NoContent }
  case _ => Pass
}

In the first line, we indicate that this code wants to process only the URL of the view /customer/[id] and binds the passed identifier to the id variable (if the immutable variable can be called that at all). In the following lines, we refine the behavior depending on the type of request. Let us examine, for example, the processing of the PUT method in steps:
  • transaction { ... } : we indicate that for the duration of the body of the handler, a transaction should be opened,
  • readCustomer(req, id) : use a pre-written method that reads the request body and returns Option[Customer]
  • : this symbol deserves special attention, in fact it is a synonym for the map operation and allows you to apply some function to the Option content, if this content is,
  • DB.customer.update : the very function that we want to apply is updating the entity in the database,
  • cata(_ => Ok, BadRequest) : returns Ok if Option there is a value or BadRequest if the request could not be parsed and we have None instead of the client.

GET and DELETE requests are handled similarly.

In the second half of the handler serving requests to /customer , we will need two auxiliary functions:

  val field: PartialFunction[String, Customer => TypedExpressionNode[_]] = {
    case "id" => _.id
    case "firstName" => _.firstName
    case "lastName" => _.lastName
    case "email" => _.email
    case "birthday" => _.birthday
  }

  val ordering: PartialFunction[String, TypedExpressionNode[_] => OrderByExpression] = {
    case "asc" => _.asc
    case "desc" => _.desc
  }

These functions will be used to create order by part of the request and, most likely, rummaging in the bowels of Squeryl, they could be written easier, but this option suited me. The handler code itself:

case req@Path(Seg("customer" :: Nil)) => req match {
  case POST(_) =>
    transaction {
      readCustomer(req, nextId) ∘ DB.customer.insert ∘ ResponseJson cata(_ ~> Created, BadRequest)
    }
  case GET(_) & Params(params) =>
    transaction {
      import Params._
      val orderBy = (params.get("orderby") ∗ first orElse Some("id")) ∗ field.lift
      val order = (params.get("order") ∗ first orElse Some("asc")) ∗ ordering.lift
      val pageNum = params.get("pagenum") ∗ (first ~> int)
      val pageSize = params.get("pagesize") ∗ (first ~> int)
      val offset = ^(pageNum, pageSize)(_ * _)
      val query = from(DB.customer) {
        q => select(q) orderBy ^(orderBy, order)(_ andThen _ apply q).toList
      }
      val pagedQuery = ^(offset, pageSize)(query.page) getOrElse query
      ResponseJson(pagedQuery.toList)
    }
  case _ => Pass
}

The part related to the POST request does not carry anything new, but then we have to process the request parameters and two strange symbols appear: and ^ . The first (carefully, do not confuse it with a normal asterisk * ) is a synonym for flatMap and differs from the map fact that the function used should also return Option . Thus, we can sequentially perform several operations, each of which either successfully returns a value or returns None in case of an error. The second operator is a little more complicated and allows you to perform some operation only if all the variables used are not equal None . This allows us to sort only if both the column and the direction are specified, and break the result into pages only if both the page number and its size are specified.

That's all, all that remains is to start the server

Http(8080).plan(service).run()

and you can pick up curl to check that everything works.

Conclusion


In my opinion, the resulting web service code is compact and fairly easy to read, and this is a very important property. Naturally, it is not ideal: for example, you probably should have used scala.Either or to handle errors scalaz.Validation , and some might not like using unicode operators. In addition, behind the external simplicity, sometimes quite complex operations can be hidden, and to understand how everything works "under the hood" you will have to strain gyrus. Nevertheless, I hope that this article will encourage someone to take a closer look at Scala: even if you fail to apply this language in your work, you will surely learn something new.

The code, as expected, is posted on GitHub , and from the one given in the article, it differs only in the presence of import-s and sbt-script for assembly.

I almost forgot - at the very beginning of the article I promised that there would be monads and other evil spirits in the web service. So, flatMap (aka ) it is a monadic bind, and the operator ^ is directly related to applicative functors.

And finally, if you are in Kharkov or Saratov and want to develop interesting things using Scala and Akka, write - we are looking for competent specialists.