📜 ⬆️ ⬇️

Implementing RESTful Web Service on Scala

Last week on Habré there were two whole articles about the implementation of RESTful web services in Java. Well, let's not fall behind and write our version on Scala, with monads and applicative functors. Mature Scala developers are unlikely to find something new in this article, and Django lovers will generally say that they have this “out-of-the-box” functionality, but I hope that it will be interesting for Java developers and curious people to read.

Training


We take the problem from the previous article as a basis, but we will try to solve it so that the solution code fits on the screen. At least 40-inch and fifth font. In the end, in the 21st 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 will clarify: we are going to implement the simplest RESTful service to access the customer database. From the necessary functionality - the creation and deletion of objects in the database, as well as the paginal output of a list of all clients with the ability to sort by different fields.
')
As bricks from which we will build a house, we take:

In the course of the article, I will try to give enough explanations so that the code would be clear to people not familiar with Scala, but I do not promise that I will succeed.


To battle


Data model

First we need to decide on the data model. Squeryl allows you to set the model as a normal 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 the value is, and None , if it is not. Using Option allows you to minimize the chances of a NullPointerException and is common practice in functional programming languages ​​(especially in those in which there is no concept of null at all).

The @JsonIgnoreProperties excludes certain fields from JSON serialization. In this case, the _isPersisted field, which Squeryl added, had to be excluded.

Database schema initialization

Those who worked with JDBC know that the first thing to do is to initialize the database driver class. We will 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 specify the Squeryl library which connection factory to use. As a database we use light and fast H2 .

Now came the turn of the scheme:

 object DB extends Schema { val customer = table[Customer] } transaction { allCatch opt DB.create } 

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

JSON serialization and deserialization

To begin with, let's initialize the JSON parser so that it can work with the data types adopted 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 parseCustomerJson function parseCustomerJson JSON. Thanks to the use of allCatch opt exceptions that occurred during the parsing process will be intercepted and as a result we will get None . The second function, readCustomer , is directly related to the processing of an HTTP request — it reads the request body, turns it into an object of type Customer and sets the id field to the specified value.

It should be noted that it was not necessary to specify the type of the return value in both functions: the compiler has enough data to display the type without the programmer’s hints, but the explicitly specified type sometimes makes it easier for people to understand the code.

The reverse process — turning the Customer object (or List[Customer] ) into an HTTP response body — is also not difficult:

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

In the future, we will simply return objects of 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 a 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 the details of the Unfiltered library device; I will only 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:

 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 form /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 specify the behavior depending on the type of request. Let us analyze, for example, the processing of the PUT method in steps:

Processing GET and DELETE requests is done in the same way.

In the second half of the handler servicing 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 an order by part of the request and, most likely, having rummaged in the depths of Squeryl, they could have been written more simply, but this also suited me. 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 contain anything new, but then we have to process the request parameters and two incomprehensible symbols appear: and ^ . The first (neatly, do not confuse it with the usual asterisk * ) is a synonym for flatMap and differs from map in that the function used must also return Option . Thus, we can consistently perform several operations, each of which either returns a value successfully or returns None in case of an error. The second operator is a bit more complicated and allows you to perform some operation only if all the variables used are not equal to None . This allows us to perform sorting only if both the column and the direction are indicated, and split the result into pages only if both the page number and its size are specified.

That's all, it remains only 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 feature. Naturally, it is not perfect: for example, it was probably worth using scala.Either or scalaz.Validation for error scalaz.Validation , and someone might not like the use of Unicode operators. In addition, quite complicated operations can sometimes be hidden behind external simplicity, and in order to understand how everything is arranged “under the hood”, it is necessary to tighten the convolutions. 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 it should be, is laid out on GitHub , and it differs from the one given in the article only by the presence of import and sbt-script for building.

I almost forgot - I promised at the very beginning of the article that there will be monads and other vermin in the web service. So, flatMap (aka ) 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.

Source: https://habr.com/ru/post/150199/


All Articles