Posts Adding Logic
Post
Cancel

Adding Logic

We will add some logic and refactor in the next sections.
We will build a dish menu for our users.

Defining Routes

First, let’s add three new routes to the conf/routes file:

1
2
3
GET     /dishes            com.example.playground.dish.DishController.allDishes
GET     /dishes/:name      com.example.playground.dish.DishController.findDish(name: String)
POST    /dishes            com.example.playground.dish.DishController.createDish

Creating a Model

Now let’s add a VO (Value Object) that will represent a dish.
VOs are represented in Scala with a case class.
Add a case class com.example.playground.dish.Dish with name, description and price:

1
2
3
4
5
case class Dish(
  name: String,
  description: String,
  price: Double
)

Add a controller

Add a controller com.example.playground.dish.DishController that implements the API above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.example.playground.dish

import play.api.mvc.{AbstractController, Action, AnyContent, ControllerComponents}
import scala.collection.mutable

import play.api.libs.json.{Json, OWrites}

class DishController(
  controllerComponents: ControllerComponents,
  dishes: mutable.Set[Dish]
) extends AbstractController(controllerComponents) {

  def allDishes(): Action[AnyContent] = Action {
    implicit val dishWrites: OWrites[Dish] = Json.writes[Dish]
    Ok(Json.toJson(dishes))
  }

  def findDish(name: String): Action[AnyContent] = Action {
    val maybeDish = dishes.find(dish => dish.name == name) // can be shortened to `dishes.find(_.name == name)`
    maybeDish match {
      case Some(dish) =>
        implicit val dishWrites = Json.writes[Dish]
        Ok(Json.toJson(dish))
      case None =>
        NotFound("could not find the specified dish")
    }
  }

  def createDish(): Action[AnyContent] = Action { request =>
    request.body.asJson match {
      case Some(jsValue) =>
        val name = (jsValue \ "name").as[String]
        val description = (jsValue \ "description").as[String]
        val price = (jsValue \ "price").as[Double]
        val dishToCreate = Dish(name, description, price)
        if (dishes.contains(dishToCreate))
          Ok(s"Dish $name already exists")
        else {
          dishes += dishToCreate
          Ok(s"Added dish $name to the dish list")
        }
      case None =>
        BadRequest("Expected json as the body, but got something else")
    }
  }

}

The code will be explained shortly.

Wiring the dependencies

This controller stores the dishes in-memory, in a mutable set which you will now inject: Try to compile the code by running compile in the sbt shell. The compilation will fail, but the conf/routes file will now be compile to a new Routes class with a constructor that requires DishController.
Create a new instance of the controller in AppComponents and provide it to the constructor:

1
2
3
4
5
6
7
8
9
import scala.collection.mutable
import com.example.playground.dish.{Dish, DishController}
// ..
val dishes = mutable.Set(
  Dish("Avocado Sandwich", "Whole grain bread with Brie cheese, tomatoes and avocado", 10),
  Dish("Ice Cream", "Chocolate + Vanilla Ice Cream", 8)
)
lazy val dishController: DishController = new DishController(controllerComponents, dishes)
override def router: Router = new Routes(httpErrorHandler, homeController, dishController, assets)

Lastly, create a new package, com.example.playground.home and move app/HomeController to this package. Change the package name at conf/routes and adjust the import at com.example.playground.configuration.components.AppComponents.

Invoking the API

You can now run the server by running run in the sbt-shell.
Let’s invoke the API:

  • Get all the existing dishes by browsing to http://localhost:9000/dishes . You should see a json with 2 dishes.
  • Find a dish by browsing to http://localhost:9000/dishes/Avocado%20Sandwich
  • Create a dish, e.g by using your favorite rest client or using curl in your terminal:
    1
    2
    3
    4
    5
    6
    7
    8
    
    curl --request POST \
    --url http://localhost:9000/dishes \
    --header 'content-type: application/json' \
    --data '{
      "name": "Karaka Spicy Ramen",
      "description": "Pork tonkotsu broth, thin noodles, pork belly chashu",
      "price": 16
    }'
    

Experiment by trying to add a dish twice or finding a dish that does not exist.

How the controller works

Actions

Each method is implemented as an Action, that takes

  • A block of code that evaluates to a Result (such as Ok), like in allDishes and findDish, or takes:
  • A function from a request to a Result, like in createDish.

Serializing JSON manually

In allDishes we would like to serialize the dishes to json, and a return a result like:

1
2
3
4
5
6
7
8
9
10
11
12
[
  {
    "name": "Ice Cream",
    "description": "Chocolate + Vanilla Ice Cream",
    "price": 8
  },
  {
    "name": "Avocado Sandwich",
    "description": "Whole grain bread with Brie cheese, tomatoes and avocado",
    "price": 10
  }
]

To do so, we need to serialize the dishes instance, which has a type mutable.Set[Dish].
Play has json encoders (which serialize classes to json) for basic types, such as String, Int, Boolean.
The scala compiler can create an encoder for collection types as well, such as List[A], Set[A], mutable.Set[A], etc. as long as there is an encoder for A in scope.

Serializing supported types

This means that if we wanted to serialize an object of type mutable.Set[String] all we had to do is pass it to the toJson method like so:

1
2
val setOfStrings = mutable.Set("hello", "world")
Json.toJson(setOfStrings)

Serializing custom types

However, we would like to serialize an object of type mutable.Set[Dish] so we need to create an encoder for the Dish class.
We could have created an encoder for dish in the following cumbersome way:

1
2
3
4
5
6
7
implicit val dishWritesCumbersome: Writes[Dish] = new Writes[Dish] {
      def writes(dish: Dish) = Json.obj(
        "name" -> dish.name,
        "description" -> dish.description,
        "price" -> dish.price
      )
    }

But this requires too much boilerplate, which means that this is too error prone. e.g, we can easily mix up the description field: "description" -> dish.name.

Serializing JSON with macros

Since all the members in the Dish case class have encoders we can use the macro Json.writes[Dish] to automatically create an encoder for it.
If a member of Dish didn’t have an encoder, but was itself composed of members that have encoders, then we could build an encoder for it in the same way. e.g, Imagine a different version of Dish, that specifies the chef:

1
2
3
4
5
case class Chef(first: String, last: String)
case class DishV2(name: String, description: String, price: Int, chef: Chef)
// later on in the code:
implicit val chefWrites: OWrites[Chef] = Json.writes[Chef]
implicit val dishV2Writes: OWrites[DishV2] = Json.writes[DishV2]

Check the signature of toJson method by clicking with the cursor on toJson and using ctrl + j, which will open the quick documentation pane (use ctrl and not cmd. if you changed your key map, use double shift, that is bound in all the key maps to search everywhere, and search for quick documentation).

We can see that the signature is

1
def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue

toJson has a type parameter T and accepts 2 parameter lists, each containing a single parameter.
In scala, a method with multiple parameters can have a single parameter list or multiple parameter lists. e.g

1
2
3
4
5
def method1(a: String, b: String): String = a + b // single parameter list that contains 2 parameters
def method2(a: String)(b: String): String = a + b // 2 parameter lists, each containing a single parameter

method1("hello", " world")
method2("hello")(" world")

The signature of toJson means that it can accept an object o of any type T if we (or the scala compiler) also pass it a json seralizer instance tjs of type Writes[T].
The type parameter T in our case is inferred as mutable.Set[Dish], so these 2 calls are equivalent:

1
2
Json.toJson(dishes)
Json.toJson[mutable.Set[Dish]](dishes)

We are not passing this method the parameters for the second parameter list.
Normally, the Scala compiler emit an error and say that we are missing an argument list:

1
 method2("hello") // compile time error: missing argument list for method method2

However, the second argument list for toJson is marked with implicit, which means that tjs is an implicit parameter of type Writes[mutable.Set[Dish]], and before the compiler would give up, it will try to find an implicit value of that type, or to create one recursively.

In our case, the compiler can provide an implicit value of type Writes[mutable.Set[Dish]] since it is given an implicit value of type Writes[Dish].

Note: Implicit value just serves as a canonical instance for its type. e.g the implicit value for Ordering[Int] just orders integers from low to high.

The findDish method searches for a dish with the same name and returns it in a similar manner, with a Writes[Dish] instance.
e.g the following code is equivalent:

1
2
3
Json.toJson(dish)
Json.toJson[Dish](dish)
Json.toJson[Dish](dish)(dishWrites)

The reason that we don’t get an error on the first and second call is that before the compiler throws an error that the second argument list was not supplied, it tries to find an implicit value of type Write[Dish] in scope, and succeeds since there is such instance that is marked with the implicit keyword.
Try to delete this keyword and check the error:

1
2
3
// implicit val dishWrites = Json.writes[Dish]
val dishWrites = Json.writes[Dish]
Json.toJson(dish) // error: No Json serializer found for type com.example.playground.dish.Dish. Try to implement an implicit Writes or Format for this type

Deserializing JSON: explanation of the initial implementation

The createDish method is implemented by providing the Action a function from a request to a result.
All we had to do to achieve this is to add request => in the beginning of the body.

Click with the cursor on request and open quick documentation (ctrl+j). Notice that the type of request is Request[AnyContent], which means that the body can be of any type (we will improve this later. we want the body to be of type Dish, since it contains a json of type Dish).

Here, we are manually creating a Dish instance by traversing the json object, looking for each field by name.
This is error prone and unfortunately code like this is common.
Sometimes the logic of deserializing the json can be nested inside several functions, and when we try to understand what a controller is doing by reading its signature we don’t even know how the body looks like.

Deserializing JSON: take 2

Let’s change the code under the first case clause to try to class-up the json automatically to Dish:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def createDish(): Action[AnyContent] = Action { request =>
  request.body.asJson match {
    case Some(jsValue) =>
      implicit val dishReads = Json.reads[Dish]
      Json.fromJson[Dish](jsValue) match {
        case JsSuccess(dishToCreate, path) =>
          if (dishes.contains(dishToCreate))
            Ok(s"Dish ${dishToCreate.name} already exists")
          else {
            dishes += dishToCreate
            Ok(s"Added dish ${dishToCreate.name} to the dish list")
          }
        case JsError(errors) =>
          BadRequest("Expected dish json, but got another json")
      }
    case None =>
      BadRequest("Expected json as the body, but got something else")
  }
}

Deserializing JSON: take 3

Let’s use Play to parse the body to json for us. We will do so by providing a json parser as the first argument (and in a new argument list) to Action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def createDish(): Action[JsValue] = Action(parse.json) { request =>
  val jsValue: JsValue = request.body
  implicit val dishReads = Json.reads[Dish]
  Json.fromJson[Dish](jsValue) match {
    case JsSuccess(dishToCreate, path) =>
      if (dishes.contains(dishToCreate))
        Ok(s"Dish ${dishToCreate.name} already exists")
      else {
        dishes += dishToCreate
        Ok(s"Added dish ${dishToCreate.name} to the dish list")
      }
    case JsError(errors) =>
      BadRequest("Expected dish json, but got another json")
  }
}

Now we no longer need to check if the body can be parsed to json or not.
Our server will automatically return a 400 Bad Request if the body is not a json.
Another benefit is that now a new developer that reads the signature of createDish can tell that its body has to be json, since it returns Action[JsValue].
You can also verify (with quick documentation) that the type of request is now Request[JsValue].
However, we still need to check if the json is a Dish json or another json (e.g it may be a person json:
{ "name": "alice", "age": 20 }).

Deserializing JSON: take 4

Let’s refactor and make the dishReads to be a member of the controller, and pass it to the body parser:

1
2
3
4
5
6
7
8
9
10
11
implicit val dishReads: Reads[Dish] = Json.reads[Dish]

def createDish(): Action[Dish] = Action(parse.json[Dish]) { request =>
  val dishToCreate: Dish = request.body
  if (dishes.contains(dishToCreate))
    Ok(s"Dish ${dishToCreate.name} already exists")
  else {
    dishes += dishToCreate
    Ok(s"Added dish ${dishToCreate.name} to the dish list")
  }
}

As a note, again, the following code is equivalent:

1
2
3
Action(parse.json[Dish]) // dishReads is passed implicitly
Action(parse.json[Dish](dishReads)) // dishReads is passed explicitly, the type parameter is passed explicitly as Dish
Action(parse.json(dishReads)) // dishReads is passed explicitly and the type patameter in inferred

Now a developer that reads the signature for createDish can immediately tell that the body is expected to be of type Dish.
We have come a long way since the first implementation.
Now we just deserialize the request body to the desired type, do some logic, and serialize the result.

Refining the signature of actions that do not read the request

Lastly, we will change the other actions to have a type signature that conveys that they don’t use the request body (by returning Action[Unit]).
We can already tell that these methods do not parse the body, since they don’t take the request as a parameter, so choose your preferred implementation:

1
2
3
4
5
6
7
def allDishes(): Action[Unit] = Action(parse.empty) { _ =>
  /* code */
}
 
def findDish(name: String): Action[Unit] = Action(parse.empty) { _ =>
  /* code */
}

We could have named the parameter of the function literal (from Request[Unit] to Result) that we are passing. e.g
Action(parse.empty) { request => ... or
Action(parse.empty) { requestWithUnitBody => ...
We named it _, which is useful since we are not using it and we have no reason to name it with a special name.