Building a Simple REST API with Scala & Play! (Part 2)
Welcome back!
[code language=”scala”]
Ok(views.html.index("Your new application is ready."))
[/code]
[code language=”scala”]
Ok("Your new application is ready.")
[/code]
Creating the controller
[code language=”scala”]
package controllers
import play.api.mvc._
class Widgets extends Controller {
def index = TODO
def create = TODO
def read(id: String) = TODO
def update(id: String) = TODO
def delete(id: String) = TODO
}
[/code]
[code]
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / controllers.Application.index
GET /cleanup controllers.Application.cleanup
#Widgets
GET /api/widgets controllers.Widgets.index
GET /api/widget/:id controllers.Widgets.read(id: String)
POST /api/widget controllers.Widgets.create
DELETE /api/widget/:id controllers.Widgets.delete(id: String)
PATCH /api/widget/:id controllers.Widgets.update(id: String)
[/code]
Data access
[code language=”scala”]
package repos
import javax.inject.Inject
import play.api.libs.json.{JsObject, Json}
import play.modules.reactivemongo.ReactiveMongoApi
import play.modules.reactivemongo.json._
import play.modules.reactivemongo.json.collection.JSONCollection
import reactivemongo.api.ReadPreference
import reactivemongo.api.commands.WriteResult
import reactivemongo.bson.{BSONDocument, BSONObjectID}
import scala.concurrent.{ExecutionContext, Future}
trait WidgetRepo {
def find()(implicit ec: ExecutionContext): Future[List[JsObject]]
def select(selector: BSONDocument)(implicit ec: ExecutionContext): Future[Option[JsObject]]
def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]
def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]
def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult]
}
[/code]
[code language=”scala”]
class WidgetRepoImpl @Inject() (reactiveMongoApi: ReactiveMongoApi) extends WidgetRepo {
def collection = reactiveMongoApi.db.collection[JSONCollection]("widgets");
override def find()(implicit ec: ExecutionContext): Future[List[JsObject]] = {
val genericQueryBuilder = collection.find(Json.obj());
val cursor = genericQueryBuilder.cursor[JsObject](ReadPreference.Primary);
cursor.collect[List]()
}
override def select(selector: BSONDocument)(implicit ec: ExecutionContext): Future[Option[JsObject]] = {
collection.find(selector).one[JsObject]
}
override def update(selector: BSONDocument, update: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
collection.update(selector, update)
}
override def remove(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
collection.remove(document)
}
override def save(document: BSONDocument)(implicit ec: ExecutionContext): Future[WriteResult] = {
collection.update(BSONDocument("_id" -> document.get("_id").getOrElse(BSONObjectID.generate)), document, upsert = true)
}
}
[/code]
Again, there shouldn’t be much that’s surprising here other than the normal, somewhat complex IMO, Scala syntax. You can see the implicit ExecutionContext, which, for asynchronous code, basically let’s Scala decide where in the thread pool to execute the related function. You may also notice the ReadPreference in the find() function. This tells Mongo that our repo would like to read it’s results from the primary Mongo node.
Back to the controller
[code language=”scala”]
package controllers
import javax.inject.Inject
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.Json
import play.api.mvc._
import play.modules.reactivemongo.{MongoController, ReactiveMongoApi, ReactiveMongoComponents}
import reactivemongo.api.commands.WriteResult
import reactivemongo.bson.{BSONObjectID, BSONDocument}
import repos.WidgetRepoImpl
class Widgets @Inject()(val reactiveMongoApi: ReactiveMongoApi) extends Controller
with MongoController with ReactiveMongoComponents {
def widgetRepo = new WidgetRepoImpl(reactiveMongoApi)
[/code]
In ‘app/controllers/Widgets.scala’, we have an unimplemented ‘index’ function. This is meant to display a list of all widgets in the database, selected without parameters. The implementation for this function is very straightforward. Update the index function such that:
[code language=”scala”]
def index = Action.async { implicit request =>
widgetRepo.find().map(widgets => Ok(Json.toJson(widgets)))
}
[/code]
[code language=”scala”]
def read(id: String) = Action.async { implicit request =>
widgetRepo.select(BSONDocument(Id -> BSONObjectID(id))).map(widget => Ok(Json.toJson(widget)))
}
[/code]
The ‘delete’ method is similarly straightforward in that it takes a String id for the document to be deleted, and returns a HTTP status code 202 (Accepted), and no body.
[code language=”scala”]
def delete(id: String) = Action.async {
widgetRepo.remove(BSONDocument(Id -> BSONObjectID(id)))
.map(result => Accepted)
}
[/code]
The ‘create’ and ‘update’ methods introduce a small amount of complexity in that they require the request body to be parsed using Scala’s built in pattern matching functionality. Since we’ll have to match the field names in two places, we’ll create a companion object to hold our field names. Create a ‘WidgetFields’ companion object in the Widgets controller source file:
[code language=”scala”]
object WidgetFields {
val Id = "_id"
val Name ="name"
val Description = "description"
val Author = "author"
}
[/code]
In the body of the Widget controller, add a scoped import for the companion object:
[code language=”scala”]
import controllers.WidgetFields._
[/code]
For the ‘create’ method, we’ll apply a JSON Body Parser to an implicit request, parsing out the relevant content needed to build a BSONDocument that can be persisted via the Repo:
[code language=”scala”]
def create = Action.async(BodyParsers.parse.json) { implicit request =>
val name = (request.body Name).as[String]
val description = (request.body Description).as[String]
val author = (request.body Author).as[String]
recipeRepo.save(BSONDocument(
Name -> name,
Description -> description,
Author -> author
)).map(result => Created)
}
[/code]
Execution of this method returns the HTTP status code 201. For the ‘update’ method, we’ll perform largely the same operation – applying the JSON Body Parser to an implicit request, however, this time we’ll call a different repo method – this time building a BSONDocument to select the relevant document with, then passing in the current field values:
[code language=”scala”]
def update(id: String) = Action.async(BodyParsers.parse.json) { implicit request =>
val name = (request.body Name).as[String]
val description = (request.body Description).as[String]
val author = (request.body Author).as[String]
widgetRepo.update(BSONDocument(Id -> BSONObjectID(id)),
BSONDocument("$set" -> BSONDocument(Name -> name, Description -> description, Author -> author)))
.map(result => Accepted)
}
[/code]
Testing!
Part 3 of this series will cover testing using the Spec2 library. In the mean time…We have a fully functioning REST API – but testing manually requires configuration and the execution of HTTP operations. Many web frameworks are packed with functionality that allows a developer to ‘bootstrap’ an application – adding seed data to a local environment for testing as an example. Recent changes in the Play! framework’s GlobalSettings have changed the way developers do things like seed test databases (https://www.playframework.com/documentation/2.4.x/GlobalSettings). While the dust settles, and while we wait for part 3, I created some helper functions in the Application controller that will create and remove some test data:
[code language=”scala”]
package controllers
import javax.inject.{Inject, Singleton}
import play.api.Logger
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.Json
import play.api.mvc.{Action, Controller}
import play.modules.reactivemongo.json.collection.JSONCollection
import play.modules.reactivemongo.{MongoController, ReactiveMongoApi, ReactiveMongoComponents}
import reactivemongo.api.collections.bson.BSONCollection
import reactivemongo.api.commands.bson.BSONCountCommand.{ Count, CountResult }
import reactivemongo.api.commands.bson.BSONCountCommandImplicits._
import reactivemongo.bson.BSONDocument
import scala.concurrent.Future
@Singleton
class Application @Inject()(val reactiveMongoApi: ReactiveMongoApi) extends Controller
with MongoController with ReactiveMongoComponents {
def jsonCollection = reactiveMongoApi.db.collection[JSONCollection]("widgets");
def bsonCollection = reactiveMongoApi.db.collection[BSONCollection]("widgets");
def index = Action {
Logger.info("Application startup…")
val posts = List(
Json.obj(
"name" -> "Widget One",
"description" -> "My first widget",
"author" -> "Justin"
),
Json.obj(
"name" -> "Widget Two: The Return",
"description" -> "My second widget",
"author" -> "Justin"
))
val query = BSONDocument("name" -> BSONDocument("$exists" -> true))
val command = Count(query)
val result: Future[CountResult] = bsonCollection.runCommand(command)
result.map { res =>
val numberOfDocs: Int = res.value
if(numberOfDocs > 1) {
jsonCollection.bulkInsert(posts.toStream, ordered = true).foreach(i => Logger.info("Record added."))
}
}
Ok("Your database is ready.")
}
def cleanup = Action {
jsonCollection.drop().onComplete {
case _ => Logger.info("Database collection dropped")
}
Ok("Your database is clean.")
}
}
[/code]
… and routes …
[code language=”scala”]
# Home page
GET / controllers.Application.index
GET /cleanup controllers.Application.cleanup
[/code]