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 it will be interesting for Java developers and just curious 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 to access the customer database. Of the necessary functionality is 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 Noneif it is not. Use Optionallows you to minimize the chances of appearing NullPointerExceptionand is common practice in functional programming languages ​​(especially those in which nullthere is no concept at all).

    Annotation @JsonIgnorePropertiesexcludes certain fields from JSON serialization. In this case, it was necessary to exclude the field _isPersistedthat 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.createthrow an exception, which we, thanks to allCatch opt, will successfully ignore.

    JSON serialization and deserialization

    To begin, 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 parseCustomerJsonactually parses JSON. Through the use of allCatch optexceptions that arose in the process of parsing, will be caught and as a result we get None. The second function,, readCustomeris directly related to the processing of the HTTP request - it reads the request body, turns it into an object of type Customerand sets the field idto 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 - turning an object Customer(or list List[Customer]) into the body of an HTTP response - 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 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 the details of the Unfiltered library, I’ll just say that the simplest way to use it is:

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

    Our service will have two entry points: /customerand /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 only wants to process 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 we want to apply is updating the entity in the database,
    • cata(_ => Ok, BadRequest): returns Okif Optionthere is a value or BadRequestif the request could not be parsed and we have Noneinstead 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 bypart of the request and, most likely, rummaging in the bowels of Squeryl, they could be written easier, but this option also 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 flatMapand differs from the mapfact 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 Nonein 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 equalNone. 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.Eitheror to handle errors scalaz.Validation, and some might not like the use of 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 it differs from the one given in the article only by 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 ) this 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.

    Also popular now: