├── conf
├── application.conf
├── generated.keystore
├── routes
├── secure.conf
├── logback.xml
└── firebaseCredentials.json
├── project
├── build.properties
├── plugins.sbt
└── Common.scala
├── Procfile
├── .gitignore
├── app
├── v1
│ └── signature
│ │ ├── FirebaseException.scala
│ │ ├── package.scala
│ │ ├── Signature.scala
│ │ ├── Firebase.scala
│ │ ├── SignatureRouter.scala
│ │ ├── SignatureAction.scala
│ │ ├── SignatureController.scala
│ │ ├── SignatureResourceHandler.scala
│ │ └── SignatureRepository.scala
├── controllers
│ └── HomeController.scala
├── views
│ └── index.scala.html
├── Module.scala
├── RequestHandler.scala
└── ErrorHandler.scala
├── public
└── stylesheets
│ ├── .crunch
│ ├── main.css
│ ├── main.less
│ └── main.css.map
├── LICENSE
├── docs
├── build.sbt
└── src
│ └── main
│ └── paradox
│ ├── appendix.md
│ ├── index.md
│ └── part-1
│ └── index.md
├── .travis.yml
├── gatling
└── simulation
│ └── GatlingSpec.scala
└── README.md
/conf/application.conf:
--------------------------------------------------------------------------------
1 | include "secure"
2 |
3 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.15
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: target/universal/stage/bin/etherlinks-backend -Dhttp.port=${PORT}
2 |
--------------------------------------------------------------------------------
/conf/generated.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/540/etherlinks-backend/master/conf/generated.keystore
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | target
3 | /.idea
4 | /.idea_modules
5 | /.classpath
6 | /.project
7 | /.settings
8 | /RUNNING_PID
9 |
10 | /.vscode
11 |
--------------------------------------------------------------------------------
/app/v1/signature/FirebaseException.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | /**
4 | * Created by pabloalbizu on 22/4/17.
5 | */
6 | case class FirebaseException(s: String) extends Exception(s)
7 |
--------------------------------------------------------------------------------
/conf/routes:
--------------------------------------------------------------------------------
1 | GET / controllers.HomeController.index
2 |
3 | -> /v1/signatures v1.signature.SignatureRouter
4 |
5 | # Map static resources from the /public folder to the /assets URL path
6 | GET /assets/*file controllers.Assets.at(path="/public", file)
7 |
8 |
--------------------------------------------------------------------------------
/app/controllers/HomeController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import play.api.mvc.{Action, Controller}
4 |
5 | /**
6 | * A very small controller that renders a home page.
7 | */
8 | class HomeController extends Controller
9 | {
10 | def index = Action { implicit request =>
11 | Ok(views.html.index())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/v1/signature/package.scala:
--------------------------------------------------------------------------------
1 | package v1
2 |
3 | import play.api.i18n.Messages
4 |
5 | /**
6 | * Package object for post. This is a good place to put implicit conversions.
7 | */
8 | package object signature {
9 |
10 | /**
11 | * Converts between PostRequest and Messages automatically.
12 | */
13 | implicit def requestToMessages[A](implicit r: SignatureRequest[A]): Messages = {
14 | r.messages
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/public/stylesheets/.crunch:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.less": {
4 | "engines": [
5 | {
6 | "compiler": "less",
7 | "output": "main.css",
8 | "options": {
9 | "compress": false,
10 | "ieCompat": false,
11 | "strictMath": false,
12 | "strictUnits": false,
13 | "javascriptEnabled": false,
14 | "sourceMap": true
15 | }
16 | }
17 | ],
18 | "sources": []
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @()
2 |
3 |
4 |
5 |
6 |
7 | Etherlinks
8 |
9 |
10 |
11 | Etherlinks REST API
12 |
13 |
14 | This is a placeholder page to show you the REST API.
15 |
16 |
17 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | License
2 | -------
3 | Written in 2016 by Lightbend
4 |
5 | To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
6 |
7 | You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see .
8 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.14")
3 |
4 | // sbt-paradox, used for documentation
5 | addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1")
6 |
7 | // Load testing tool:
8 | // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html
9 | addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0")
10 |
11 | // Scala formatting: "sbt scalafmt"
12 | // https://olafurpg.github.io/scalafmt
13 | addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.3.1")
14 |
--------------------------------------------------------------------------------
/docs/build.sbt:
--------------------------------------------------------------------------------
1 | // You will need private bintray credentials to publish this with Lightbend theme
2 | // credentials += Credentials("Bintray", "dl.bintray.com", "", "")
3 | // resolvers += "bintray-typesafe-internal-maven-releases" at "https://dl.bintray.com/typesafe/internal-maven-releases/"
4 | // paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.3")
5 |
6 | // Uses the out of the box generic theme.
7 | paradoxTheme := Some(builtinParadoxTheme("generic"))
8 |
9 | scalaVersion := "2.11.11"
10 |
--------------------------------------------------------------------------------
/app/v1/signature/Signature.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import scala.beans.BeanProperty
4 |
5 | case class Signature(uri: String, transaction_id: String, date: Long) {
6 | def toBean = {
7 | val signature = new SignatureBean()
8 | signature.uri = uri
9 | signature.transaction_id = transaction_id
10 | signature.date = date
11 | signature
12 | }
13 |
14 | }
15 |
16 | class SignatureBean() {
17 | @BeanProperty var uri:String = null
18 | @BeanProperty var transaction_id:String = null
19 | @BeanProperty var date:Long = 0
20 | }
21 |
--------------------------------------------------------------------------------
/app/Module.scala:
--------------------------------------------------------------------------------
1 | import javax.inject._
2 |
3 | import com.google.inject.AbstractModule
4 | import net.codingwell.scalaguice.ScalaModule
5 | import play.api.{Configuration, Environment}
6 | import v1.signature._
7 |
8 | /**
9 | * Sets up custom components for Play.
10 | *
11 | * https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection
12 | */
13 | class Module(environment: Environment, configuration: Configuration)
14 | extends AbstractModule
15 | with ScalaModule {
16 |
17 | override def configure() = {
18 | bind[SignatureRepository].to[SignatureRepositoryImpl].in[Singleton]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/v1/signature/Firebase.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import java.io.InputStream
4 |
5 | import com.google.firebase.{FirebaseApp, FirebaseOptions}
6 | import com.google.firebase.database._
7 |
8 | object Firebase {
9 | private val credentials : InputStream = getClass.getResourceAsStream("/firebaseCredentials.json")
10 | private val options = new FirebaseOptions.Builder()
11 | .setDatabaseUrl("https://etherlinks-7503a.firebaseio.com")
12 | .setServiceAccount(credentials)
13 | .build()
14 |
15 | FirebaseApp.initializeApp(options)
16 | private val database = FirebaseDatabase.getInstance()
17 | def ref(path: String): DatabaseReference = database.getReference(path)
18 | }
19 |
--------------------------------------------------------------------------------
/app/v1/signature/SignatureRouter.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.routing.Router.Routes
6 | import play.api.routing.SimpleRouter
7 | import play.api.routing.sird._
8 |
9 | /**
10 | * Routes and URLs to the PostResource controller.
11 | */
12 | class SignatureRouter @Inject()(controller: SignatureController) extends SimpleRouter {
13 | val prefix = "/v1/signatures"
14 |
15 | def link(id: SignatureId): String = {
16 | import com.netaporter.uri.dsl._
17 | val url = prefix / id.toString
18 | url.toString()
19 | }
20 |
21 | override def routes: Routes = {
22 | case GET(p"/") =>
23 | controller.index
24 |
25 | case POST(p"/") =>
26 | controller.process
27 |
28 | case GET(p"/$id") =>
29 | controller.show(id)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/conf/secure.conf:
--------------------------------------------------------------------------------
1 | # Set up Play for HTTPS and locked down allowed hosts.
2 | # Nothing in here is required for REST, but it's a good default.
3 | play {
4 | crypto.secret = "67887bb1-231a-47f6-b5fb-9fd45ee9ea27"
5 |
6 | http {
7 | cookies.strict = true
8 |
9 | session.secure = true
10 | session.httpOnly = true
11 |
12 | flash.secure = true
13 | flash.httpOnly = true
14 |
15 | forwarded.trustedProxies = ["::1", "127.0.0.1"]
16 | }
17 |
18 | i18n {
19 | langCookieSecure = true
20 | langCookieHttpOnly = true
21 | }
22 |
23 | filters {
24 | csrf {
25 | cookie.secure = true
26 | }
27 |
28 | hosts {
29 | allowed = ["localhost:9443", "localhost:9000"]
30 | }
31 |
32 | hsts {
33 | maxAge = 1 minute # don't interfere with other projects
34 | secureHost = "localhost"
35 | securePort = 9443
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/project/Common.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 | import sbt.plugins.JvmPlugin
4 |
5 | /**
6 | * Settings that are comment to all the SBT projects
7 | */
8 | object Common extends AutoPlugin {
9 | override def trigger = allRequirements
10 | override def requires: sbt.Plugins = JvmPlugin
11 |
12 | override def projectSettings = Seq(
13 | organization := "com.lightbend.restapi",
14 | version := "1.0-SNAPSHOT",
15 | resolvers += Resolver.typesafeRepo("releases"),
16 | javacOptions ++= Seq("-source", "1.8", "-target", "1.8"),
17 | scalacOptions ++= Seq(
18 | "-encoding",
19 | "UTF-8", // yes, this is 2 args
20 | "-target:jvm-1.8",
21 | "-deprecation",
22 | "-feature",
23 | "-unchecked",
24 | "-Xlint",
25 | "-Yno-adapted-args",
26 | "-Ywarn-numeric-widen",
27 | "-Xfatal-warnings"
28 | ),
29 | scalacOptions in Test ++= Seq("-Yrangepos"),
30 | autoAPIMappings := true
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | dist: trusty
3 | sudo: true
4 | group: beta
5 | scala:
6 | - 2.11.8
7 | jdk:
8 | - oraclejdk8
9 | cache:
10 | directories:
11 | - "$HOME/.ivy2/cache"
12 | - "$HOME/.sbt/boot/"
13 | before_cache:
14 | - rm -rf $HOME/.ivy2/cache/com.typesafe.play/*
15 | - rm -rf $HOME/.ivy2/cache/scala_*/sbt_*/com.typesafe.play/*
16 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print0 | xargs -n10 -0 rm
17 | - find $HOME/.sbt -name "*.lock" -delete
18 | notifications:
19 | slack:
20 | secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU=
21 |
--------------------------------------------------------------------------------
/gatling/simulation/GatlingSpec.scala:
--------------------------------------------------------------------------------
1 | package simulation
2 |
3 | import io.gatling.core.Predef._
4 | import io.gatling.http.Predef._
5 | import scala.concurrent.duration._
6 | import scala.language.postfixOps
7 |
8 | // run with "sbt gatling:test" on another machine so you don't have resources contending.
9 | // http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure
10 | class GatlingSpec extends Simulation {
11 |
12 | // change this to another machine, make sure you have Play running in producion mode
13 | // i.e. sbt stage / sbt dist and running the script
14 | val httpConf = http.baseURL("http://localhost:9000/v1/sigantures")
15 |
16 | val readClients = scenario("Clients").exec(Index.refreshManyTimes)
17 |
18 | setUp(
19 | // For reference, this hits 25% CPU on a 5820K with 32 GB, running both server and load test.
20 | // In general, you want to ramp up load slowly, and measure with a JVM that has been "warmed up":
21 | // https://groups.google.com/forum/#!topic/gatling/mD15aj-fyo4
22 | readClients.inject(rampUsers(10000) over (100 seconds)).protocols(httpConf)
23 | )
24 | }
25 |
26 | object Index {
27 |
28 | def refreshAfterOneSecond =
29 | exec(http("Index").get("/").check(status.is(200))).pause(1)
30 |
31 | val refreshManyTimes = repeat(10000) {
32 | refreshAfterOneSecond
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ${application.home:-.}/logs/application.log
8 |
9 | %date [%level] from %logger in %thread - %message%n%xException
10 |
11 |
12 |
13 |
14 |
15 | ${application.home:-.}/logs/metrics.log
16 |
17 | %date [%level] from %logger in %thread - %message%n%xException
18 |
19 |
20 |
21 |
22 |
23 | %coloredLevel %logger{15} - %message%n%xException{10}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/RequestHandler.scala:
--------------------------------------------------------------------------------
1 | import javax.inject.Inject
2 |
3 | import play.api.http._
4 | import play.api.mvc._
5 | import play.api.routing.Router
6 |
7 | /**
8 | * Handles all requests.
9 | *
10 | * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler
11 | */
12 | class RequestHandler @Inject()(router: Router,
13 | errorHandler: HttpErrorHandler,
14 | configuration: HttpConfiguration,
15 | filters: HttpFilters)
16 | extends DefaultHttpRequestHandler(router,
17 | errorHandler,
18 | configuration,
19 | filters) {
20 |
21 | override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = {
22 | super.handlerForRequest {
23 | // ensures that REST API does not need a trailing "/"
24 | if (isREST(request)) {
25 | addTrailingSlash(request)
26 | } else {
27 | request
28 | }
29 | }
30 | }
31 |
32 | private def isREST(request: RequestHeader) = {
33 | request.uri match {
34 | case uri: String if uri.contains("post") => true
35 | case _ => false
36 | }
37 | }
38 |
39 | private def addTrailingSlash(origReq: RequestHeader): RequestHeader = {
40 | if (!origReq.path.endsWith("/")) {
41 | val path = origReq.path + "/"
42 | if (origReq.rawQueryString.isEmpty) {
43 | origReq.copy(path = path, uri = path)
44 | } else {
45 | origReq.copy(path = path, uri = path + s"?${origReq.rawQueryString}")
46 | }
47 | } else {
48 | origReq
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/v1/signature/SignatureAction.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.http.HttpVerbs
6 | import play.api.i18n.{Messages, MessagesApi}
7 | import play.api.mvc._
8 |
9 | import scala.concurrent.{ExecutionContext, Future}
10 |
11 | /**
12 | * A wrapped request for post resources.
13 | *
14 | * This is commonly used to hold request-specific information like
15 | * security credentials, and useful shortcut methods.
16 | */
17 | class SignatureRequest[A](request: Request[A], val messages: Messages)
18 | extends WrappedRequest(request)
19 |
20 | /**
21 | * The default action for the Post resource.
22 | *
23 | * This is the place to put logging, metrics, to augment
24 | * the request with contextual data, and manipulate the
25 | * result.
26 | */
27 | class SignatureAction @Inject()(messagesApi: MessagesApi)(
28 | implicit ec: ExecutionContext)
29 | extends ActionBuilder[SignatureRequest]
30 | with HttpVerbs {
31 |
32 | type PostRequestBlock[A] = SignatureRequest[A] => Future[Result]
33 |
34 | private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass)
35 |
36 | override def invokeBlock[A](request: Request[A],
37 | block: PostRequestBlock[A]): Future[Result] = {
38 | if (logger.isTraceEnabled()) {
39 | logger.trace(s"invokeBlock: request = $request")
40 | }
41 |
42 | val messages = messagesApi.preferred(request)
43 | val future = block(new SignatureRequest(request, messages))
44 |
45 | future.map { result =>
46 | request.method match {
47 | case GET | HEAD =>
48 | result.withHeaders("Cache-Control" -> s"max-age: 100")
49 | case other =>
50 | result
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/v1/signature/SignatureController.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.data.Form
6 | import play.api.libs.json.Json
7 | import play.api.mvc._
8 |
9 | import scala.concurrent.{ExecutionContext, Future}
10 |
11 | case class SignatureFormInput(uri: String)
12 |
13 | /**
14 | * Takes HTTP requests and produces JSON.
15 | */
16 | class SignatureController @Inject()(
17 | action: SignatureAction,
18 | handler: SignatureResourceHandler)(implicit ec: ExecutionContext)
19 | extends Controller {
20 |
21 | private val form: Form[SignatureFormInput] = {
22 | import play.api.data.Forms._
23 |
24 | Form(
25 | mapping(
26 | "uri" -> nonEmptyText
27 | )(SignatureFormInput.apply)(SignatureFormInput.unapply)
28 | )
29 | }
30 |
31 | def index: Action[AnyContent] = {
32 | action.async { implicit request =>
33 | handler.find.map { signatures =>
34 | Ok(Json.toJson(signatures))
35 | }
36 | }
37 | }
38 |
39 | def process: Action[AnyContent] = {
40 | action.async { implicit request =>
41 | processJsonPost()
42 | }
43 | }
44 |
45 | def show(id: String): Action[AnyContent] = {
46 | action.async { implicit request =>
47 | handler.lookup(id).map { signature =>
48 | Ok(Json.toJson(signature))
49 | }
50 | }
51 | }
52 |
53 | private def processJsonPost[A]()(
54 | implicit request: SignatureRequest[A]): Future[Result] = {
55 | def failure(badForm: Form[SignatureFormInput]) = {
56 | Future.successful(BadRequest(badForm.errorsAsJson))
57 | }
58 |
59 | def success(input: SignatureFormInput) = {
60 | handler.create(input).map { signature =>
61 | Created(Json.toJson(signature)).withHeaders(LOCATION -> signature.uri)
62 | }
63 | }
64 |
65 | form.bindFromRequest().fold(failure, success)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 60px;
3 | background-color: #fbfcfe;
4 | }
5 | .navbar-default {
6 | background-color: #797979;
7 | }
8 | .navbar-default .navbar-brand {
9 | color: #eeeeee;
10 | }
11 | .navbar-default .navbar-brand:hover,
12 | .navbar-default .navbar-brand:focus {
13 | background-color: #868686;
14 | color: white;
15 | }
16 | .navbar-default .navbar-brand.active {
17 | background-color: #9f9f9f;
18 | color: white;
19 | }
20 | .navbar-default .navbar-nav > li > a {
21 | color: #eeeeee;
22 | }
23 | .navbar-default .navbar-nav > li > a:hover,
24 | .navbar-default .navbar-nav > li > a:focus {
25 | background-color: #868686;
26 | color: white;
27 | }
28 | .navbar-default .navbar-nav > .active > a,
29 | .navbar-default .navbar-nav > .active > a:hover,
30 | .navbar-default .navbar-nav > .active > a:focus {
31 | background-color: #9f9f9f;
32 | color: white;
33 | }
34 | .navbar-default .navbar-nav > .open > a,
35 | .navbar-default .navbar-nav > .open > a:hover,
36 | .navbar-default .navbar-nav > .open > a:focus {
37 | background-color: #9f9f9f;
38 | color: white;
39 | }
40 | .navbar-default .dropdown-menu {
41 | background-color: #9f9f9f;
42 | }
43 | .navbar-default .dropdown-menu > li > a {
44 | color: white;
45 | }
46 | .navbar-default .dropdown-menu > li > a:hover {
47 | background-color: #868686;
48 | color: white;
49 | }
50 | .container {
51 | max-width: 960px;
52 | }
53 | .footer {
54 | background-color: #ececec;
55 | margin-top: 20px;
56 | padding: 20px 0;
57 | line-height: 15px;
58 | }
59 | h3,
60 | h4 {
61 | margin-top: 35px;
62 | }
63 | h1 + h2,
64 | h2 + h3,
65 | h3 + h4 {
66 | margin-top: 15px;
67 | }
68 | .highlight {
69 | background-color: #FFFFF8;
70 | }
71 | pre code {
72 | white-space: pre;
73 | }
74 | span.def {
75 | color: #888;
76 | font-size: 0.75em;
77 | font-style: italic;
78 | margin-left: 20px;
79 | }
80 | .error dd {
81 | color: red;
82 | }
83 | .info {
84 | color: blue;
85 | }
86 |
87 | /*# sourceMappingURL=main.css.map */
--------------------------------------------------------------------------------
/app/v1/signature/SignatureResourceHandler.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import java.util.Calendar
4 | import javax.inject.{Inject, Provider}
5 |
6 | import scala.concurrent.{ExecutionContext, Future}
7 | import play.api.libs.json._
8 |
9 | import scala.util.Random
10 |
11 | /**
12 | * DTO for displaying post information.
13 | */
14 | case class SignatureResource(id: String, uri: String, date: Long)
15 |
16 | object SignatureResource {
17 |
18 | /**
19 | * Mapping to write a PostResource out as a JSON value.
20 | */
21 | implicit val implicitWrites = new Writes[SignatureResource] {
22 | def writes(signature: SignatureResource): JsValue = {
23 | Json.obj(
24 | "id" -> signature.id,
25 | "uri" -> signature.uri,
26 | "date" -> signature.date
27 | )
28 | }
29 | }
30 | }
31 |
32 | /**
33 | * Controls access to the backend data, returning [[SignatureResource]]
34 | */
35 | class SignatureResourceHandler @Inject()(
36 | routerProvider: Provider[SignatureRouter],
37 | signatureRepository: SignatureRepository)(implicit ec: ExecutionContext) {
38 |
39 | def create(signatureInput: SignatureFormInput): Future[SignatureResource] = {
40 | val data = SignatureData(SignatureId(Random.nextInt(100000).toString), signatureInput.uri, Calendar.getInstance().getTimeInMillis)
41 | // We don't actually create the post, so return what we have
42 | signatureRepository.create(data).map { id =>
43 | createPostResource(data)
44 | }
45 | }
46 |
47 | def lookup(id: String): Future[Option[SignatureResource]] = {
48 | val postFuture = signatureRepository.get(SignatureId(id))
49 | postFuture.map { maybePostData =>
50 | maybePostData.map { postData =>
51 | createPostResource(postData)
52 | }
53 | }
54 | }
55 |
56 | def find: Future[Iterable[SignatureResource]] = {
57 | signatureRepository.list().map { postDataList =>
58 | postDataList.map(postData => createPostResource(postData))
59 | }
60 | }
61 |
62 | private def createPostResource(p: SignatureData): SignatureResource = {
63 | SignatureResource(p.id.toString, p.uri, p.date)
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/public/stylesheets/main.less:
--------------------------------------------------------------------------------
1 | @nav-bg-color: #797979;
2 | @nav-bg-color-hover: lighten(@nav-bg-color, 5%);
3 | @nav-bg-color-active: lighten(@nav-bg-color, 15%);
4 | @nav-color: #eeeeee;
5 | @nav-color-hover: white;
6 | @nav-color-active: white;
7 | @bg-color: #fbfcfe;
8 | @footer-bg-color: lighten(@nav-bg-color, 45%);
9 |
10 | @code-bg-color: #FFFFF8;
11 |
12 | body {
13 | padding-top: 60px;
14 | background-color: @bg-color;
15 | }
16 |
17 | .navbar-mixin-active() {
18 | background-color: @nav-bg-color-active;
19 | color: @nav-color-active;
20 | }
21 |
22 | .navbar-mixin-hover() {
23 | background-color: @nav-bg-color-hover;
24 | color: white;
25 | }
26 |
27 | .navbar-default {
28 | background-color: @nav-bg-color;
29 | .navbar-brand {
30 | color: @nav-color;
31 | &:hover, &:focus {
32 | .navbar-mixin-hover();
33 | }
34 | &.active {
35 | .navbar-mixin-active();
36 | }
37 | }
38 | .navbar-nav {
39 | & > li > a {
40 | color: @nav-color;
41 | &:hover, &:focus {
42 | .navbar-mixin-hover();
43 | }
44 | }
45 | & > .active > a {
46 | &, &:hover, &:focus {
47 | .navbar-mixin-active();
48 | }
49 | }
50 | & > .open > a {
51 | &, &:hover, &:focus {
52 | .navbar-mixin-active();
53 | }
54 | }
55 | }
56 | .dropdown-menu {
57 | background-color: @nav-bg-color-active;
58 | & > li > a {
59 | color: @nav-color-active;
60 | &:hover {
61 | .navbar-mixin-hover();
62 | }
63 | }
64 | }
65 | }
66 |
67 | .container {
68 | max-width: 960px;
69 | }
70 |
71 | .footer {
72 | background-color: @footer-bg-color;
73 | margin-top: 20px;
74 | padding: 20px 0;
75 | line-height: 15px;
76 | }
77 |
78 | h3, h4 {
79 | margin-top: 35px;
80 | }
81 |
82 | h1 + h2, h2 + h3, h3 + h4 {
83 | margin-top: 15px;
84 | }
85 |
86 | .highlight {
87 | background-color: @code-bg-color;
88 | }
89 |
90 | pre code {
91 | white-space: pre;
92 | }
93 |
94 | span.def {
95 | color: #888;
96 | font-size: 0.75em;
97 | font-style: italic;
98 | margin-left: 20px;
99 | }
100 |
101 | .error dd {
102 | color: red;
103 | }
104 |
105 | .info {
106 | color: blue;
107 | }
108 |
--------------------------------------------------------------------------------
/conf/firebaseCredentials.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "service_account",
3 | "project_id": "etherlinks-7503a",
4 | "private_key_id": "48cd8890ab3aa0e5a6c5e2f8ecd7b80e5ebdeb35",
5 | "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9/xdT8Gf0MZve\nJdk4g9Ms9kNViWOdBvEdCAjmGwoY765CXWN5u4lC9kaGKeb/Me5Cyej8ZaAv6OvT\ndUTnpwEzQIBGpNXUpJ+svOUTDGGyDrcxHaS3fNrw15V3G6QFOf20aZzyOCL46Ms+\niXOuZkDZ97cns3ngRpGvKlQ78OdrAfsY4wRWVoq2cBkNysR7NxCQgSzdFrob2afp\njy13Je8SAYs2DLGyqeTlmZX0JGsow3YX7BLKVUl9GNFgxygpevKZDr2/G5gYCUIr\nId/0EvtIpUX5N4VLJa5YCAc/V7gQZI7ILJxqo4Ak5xg0r0D16sZSQUNBvZQjxeet\nY3UNOJgJAgMBAAECggEAP4n4pjSmvy57/t3PeCv1ynqM6CUrMA9rQRc3yqROSyqU\nr1MF+mbyL3em/CU1QDDVinZ1uTrPSFZvz2bPZV0fdKFkhLuJWzS2xZDiVu10GpT7\nRKyyMj7KBXNDY/LVdn3/T39mYkbNw0ou9joHetJta7eBBADsiElxXRAd3XHiIpGS\nAc0FpLx1o1XM2gQiPS2d83jCwjaUN7Q6yDonnNAFCOY5bIxXL04DRXGunHWR+uzk\nuRs/7hiX6roDsA2F7ljJGAhWodR8od7YTJEBHro/ibrTertaWZt/KQekjbfxOIyK\npfNWw/IescpURNgXIqnYIQV6DSSH0nudOkM/dtvFgQKBgQDveRCmQYZIFjNoHFvc\ngkfg8yfheGl40dyONnXF42J2dnTwQCSjchiLiIiptA62snKa2nmY3C2Bo1681qI3\n052d7eLShGHYscnVK3gUXJNGOWXY64IRwFHxkfVkkNU1D0taegx3I4jyaWX9z1iB\n5XCKWOzuELEUvQt0j7wrf/ABkQKBgQDLG+QU3wYgQ7YVbpbWq4egM4cIShyNhh1D\nOtmVqClxbNMRz8XJ1RgEiPA3jWs2VMJq68cltdl/jnt63wAKX2Mu7mkmzy61yECv\nbqKvgOSWifJJNiMASr0FjF+vOOTNovH9pY3CEpEFUVcxanxYjFBK0kNTQdCrwHDL\n14HF4Izy+QKBgQDAL7UA+M0/xTF0eG3wxk3RZtO2y+A5qpMLPwG90wMOd3rZ5WCC\nG+s+8FBdgbfdP9FiHbxAxVNwGyDJBKgjjZ2NRYEn2j58nhKRFdXE4ZcAYMgwErHM\ndUBOUwlRsDqr3p5m+lROuSILCjNkQqeReAWdwkhDtvmm3yD2ZvRpJMAFQQKBgCL9\nSQt3aDzCrWl/xahxoEmu1sJM0iJnSj88siMf9xO9JZdZ7b7ZYOvXE1rel+uiKmP0\n/je23iMlToxaQk2HJTT+iUrQQkG6n7oZGxmU4Mw2M3D9TIZOWcXM6ubqrC/otDt3\n+7XFMQpzesvehlFSyro5ArQjEGmmG0hidc664k0xAoGBAKV8JUaS7fgiR7yItO/B\npB5h/CJylo3fDRavRsBLX5JWPWSzgFGTZORPRtZdJIRvl7sapLkGRv6ng/CyFhUU\nATh5ddN4DqXcPv60TQMhQW8UKMD+DPRXc/1FIbQBrGcjO7mH8CfYFin4sI7ziWM4\npzCNwdVahTZrKanJeBEekVal\n-----END PRIVATE KEY-----\n",
6 | "client_email": "etherlinks-7503a@appspot.gserviceaccount.com",
7 | "client_id": "114557761044153105809",
8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9 | "token_uri": "https://accounts.google.com/o/oauth2/token",
10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/etherlinks-7503a%40appspot.gserviceaccount.com"
12 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [
](https://travis-ci.org/playframework/play-scala-rest-api-example)
2 |
3 | # Etherlinks Backend
4 |
5 | This is the backend project for Etherlinks
6 |
7 | ## Appendix
8 |
9 | ### Running
10 |
11 | You need to download and install sbt for this application to run.
12 |
13 | Once you have sbt installed, the following at the command prompt will start up Play in development mode:
14 |
15 | ```
16 | sbt run
17 | ```
18 |
19 | Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request.
20 |
21 | ### Usage
22 |
23 | If you call the same URL from the command line, you’ll see JSON. Using httpie, we can execute the command:
24 |
25 | ```
26 | http --verbose http://localhost:9000/v1/posts
27 | ```
28 |
29 | and get back:
30 |
31 | ```
32 | GET /v1/posts HTTP/1.1
33 | ```
34 |
35 | Likewise, you can also send a POST directly as JSON:
36 |
37 | ```
38 | http --verbose POST http://localhost:9000/v1/posts title="hello" body="world"
39 | ```
40 |
41 | and get:
42 |
43 | ```
44 | POST /v1/posts HTTP/1.1
45 | ```
46 |
47 | ### Load Testing
48 |
49 | The best way to see what Play can do is to run a load test. We've included Gatling in this test project for integrated load testing.
50 |
51 | Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play script:s
52 |
53 | ```
54 | sbt stage
55 | cd target/universal/stage
56 | bin/play-rest-api -Dplay.crypto.secret=testing
57 | ```
58 |
59 | Then you'll start the Gatling load test up (it's already integrated into the project):
60 |
61 | ```
62 | sbt gatling:test
63 | ```
64 |
65 | For best results, start the gatling load test up on another machine so you do not have contending resources. You can edit the [Gatling simulation](http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure), and change the numbers as appropriate.
66 |
67 | Once the test completes, you'll see an HTML file containing the load test chart:
68 |
69 | ```
70 | ./rest-api/target/gatling/gatlingspec-1472579540405/index.html
71 | ```
72 |
73 | That will contain your load test results.
74 |
--------------------------------------------------------------------------------
/app/v1/signature/SignatureRepository.scala:
--------------------------------------------------------------------------------
1 | package v1.signature
2 |
3 | import java.util.{Calendar, UUID}
4 | import javax.inject.{Inject, Singleton}
5 |
6 | import scala.concurrent.Future
7 |
8 | final case class SignatureData(id: SignatureId, uri: String, date: Long)
9 |
10 | class SignatureId private(val underlying: Int) extends AnyVal {
11 | override def toString: String = underlying.toString
12 | }
13 |
14 | object SignatureId {
15 | def apply(raw: String): SignatureId = {
16 | require(raw != null)
17 | new SignatureId(Integer.parseInt(raw))
18 | }
19 | }
20 |
21 | /**
22 | * A pure non-blocking interface for the PostRepository.
23 | */
24 | trait SignatureRepository {
25 | def create(data: SignatureData): Future[SignatureId]
26 |
27 | def list(): Future[Iterable[SignatureData]]
28 |
29 | def get(id: SignatureId): Future[Option[SignatureData]]
30 | }
31 |
32 | /**
33 | * A trivial implementation for the Post Repository.
34 | */
35 | @Singleton
36 | class SignatureRepositoryImpl @Inject() extends SignatureRepository {
37 |
38 | private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass)
39 |
40 | private val signatureList = List(
41 | SignatureData(SignatureId("1"), "https://twitter.com/the_melee/status/856059487052017664", 246237453264l),
42 | SignatureData(SignatureId("2"), "https://twitter.com/the_melee/status/856059487052017664", 246237453264l),
43 | SignatureData(SignatureId("3"), "https://twitter.com/the_melee/status/856059487052017664", 246237453264l),
44 | SignatureData(SignatureId("4"), "https://twitter.com/the_melee/status/856059487052017664", 246237453264l),
45 | SignatureData(SignatureId("5"), "https://twitter.com/the_melee/status/856059487052017664", 246237453264l)
46 | )
47 |
48 | override def list(): Future[Iterable[SignatureData]] = {
49 | Future.successful {
50 | logger.trace(s"list: ")
51 | signatureList
52 | }
53 | }
54 |
55 | override def get(id: SignatureId): Future[Option[SignatureData]] = {
56 | Future.successful {
57 | logger.trace(s"get: id = $id")
58 | signatureList.find(signature => signature.id == id)
59 | }
60 | }
61 |
62 | def create(data: SignatureData): Future[SignatureId] = {
63 | Future.successful {
64 | val signature = Signature(data.uri, UUID.randomUUID().toString, data.date)
65 |
66 | val signaturesRef = Firebase.ref("signatures/" +data.id)
67 |
68 | signaturesRef.setValue(signature.toBean)
69 |
70 | logger.trace(s"create: data = $data")
71 | data.id
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/public/stylesheets/main.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["main.less"],"names":[],"mappings":"AAWA;EACE,iBAAA;EACA,yBAAA;;AAaF;EACE,yBAAA;;AADF,eAEE;EACE,cAAA;;AACA,eAFF,cAEG;AAAQ,eAFX,cAEY;EARZ,yBAAA;EACA,YAAA;;AAUE,eALF,cAKG;EAhBH,yBAAA;EACA,YAAA;;AAoBE,eADF,YACI,KAAK;EACL,cAAA;;AACA,eAHJ,YACI,KAAK,IAEJ;AAAQ,eAHb,YACI,KAAK,IAEK;EAlBd,yBAAA;EACA,YAAA;;AAsBI,eARJ,YAOI,UAAU;AACP,eARP,YAOI,UAAU,IACN;AAAQ,eARhB,YAOI,UAAU,IACG;EA5BjB,yBAAA;EACA,YAAA;;AAgCI,eAbJ,YAYI,QAAQ;AACL,eAbP,YAYI,QAAQ,IACJ;AAAQ,eAbhB,YAYI,QAAQ,IACK;EAjCjB,yBAAA;EACA,YAAA;;AAQF,eA6BE;EACE,yBAAA;;AACA,eAFF,eAEI,KAAK;EACL,YAAA;;AACA,eAJJ,eAEI,KAAK,IAEJ;EArCL,yBAAA;EACA,YAAA;;AA2CF;EACE,gBAAA;;AAGF;EACE,yBAAA;EACA,gBAAA;EACA,eAAA;EACA,iBAAA;;AAGF;AAAI;EACF,gBAAA;;AAGF,EAAG;AAAM,EAAG;AAAM,EAAG;EACnB,gBAAA;;AAGF;EACE,yBAAA;;AAGF,GAAI;EACF,gBAAA;;AAGF,IAAI;EACF,WAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;;AAGF,MAAO;EACL,UAAA;;AAGF;EACE,WAAA","sourcesContent":["@nav-bg-color: #797979;\n@nav-bg-color-hover: lighten(@nav-bg-color, 5%);\n@nav-bg-color-active: lighten(@nav-bg-color, 15%);\n@nav-color: #eeeeee;\n@nav-color-hover: white;\n@nav-color-active: white;\n@bg-color: #fbfcfe;\n@footer-bg-color: lighten(@nav-bg-color, 45%);\n\n@code-bg-color: #FFFFF8;\n\nbody {\n padding-top: 60px;\n background-color: @bg-color;\n}\n\n.navbar-mixin-active() {\n background-color: @nav-bg-color-active;\n color: @nav-color-active;\n}\n\n.navbar-mixin-hover() {\n background-color: @nav-bg-color-hover;\n color: white;\n}\n\n.navbar-default {\n background-color: @nav-bg-color;\n .navbar-brand {\n color: @nav-color;\n &:hover, &:focus {\n .navbar-mixin-hover();\n }\n &.active {\n .navbar-mixin-active();\n }\n }\n .navbar-nav {\n & > li > a {\n color: @nav-color;\n &:hover, &:focus {\n .navbar-mixin-hover();\n }\n }\n & > .active > a {\n &, &:hover, &:focus {\n .navbar-mixin-active();\n }\n }\n & > .open > a {\n &, &:hover, &:focus {\n .navbar-mixin-active();\n }\n }\n }\n .dropdown-menu {\n background-color: @nav-bg-color-active;\n & > li > a {\n color: @nav-color-active;\n &:hover {\n .navbar-mixin-hover();\n }\n }\n }\n}\n\n.container {\n max-width: 960px;\n}\n\n.footer {\n background-color: @footer-bg-color;\n margin-top: 20px;\n padding: 20px 0;\n line-height: 15px;\n}\n\nh3, h4 {\n margin-top: 35px;\n}\n\nh1 + h2, h2 + h3, h3 + h4 {\n margin-top: 15px;\n}\n\n.highlight {\n background-color: @code-bg-color;\n}\n\npre code {\n white-space: pre;\n}\n\nspan.def {\n color: #888;\n font-size: 0.75em;\n font-style: italic;\n margin-left: 20px;\n}\n\n.error dd {\n color: red;\n}\n\n.info {\n color: blue;\n}\n"]}
--------------------------------------------------------------------------------
/docs/src/main/paradox/appendix.md:
--------------------------------------------------------------------------------
1 |
2 | # Appendix
3 |
4 | This appendix covers how to download, run, use and load test Play.
5 |
6 | ## Requirements
7 |
8 | You will need a JDK 1.8 that is more recent than b20. You can download the JDK from [here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html).
9 |
10 | You will need to have git installed.
11 |
12 | ## Downloading
13 |
14 | You can download the example project from Github:
15 |
16 | ```
17 | git clone https://github.com/playframework/play-rest-api.git
18 | ```
19 |
20 | ## Running
21 |
22 | You need to download and install sbt for this application to run. You can do that by going to the [sbt download page](http://www.scala-sbt.org/download.html) and following the instructions for your platform.
23 |
24 | Once you have sbt installed, the following at the command prompt will download any required library dependencies, and start up Play in development mode:
25 |
26 | ```
27 | sbt run
28 | ```
29 |
30 | Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. You can read more about using Play [here](https://www.playframework.com/documentation/2.5.x/PlayConsole).
31 |
32 | ## Usage
33 |
34 | If you call the same URL from the command line, you’ll see JSON. Using [httpie](https://httpie.org/), we can execute the command:
35 |
36 | ```
37 | http --verbose http://localhost:9000/v1/posts
38 | ```
39 |
40 | and get back:
41 |
42 | ```
43 | GET /v1/posts HTTP/1.1
44 | ```
45 |
46 | Likewise, you can also send a POST directly as JSON:
47 |
48 | ```
49 | http --verbose POST http://localhost:9000/v1/posts title="hello" body="world"
50 | ```
51 |
52 | and get:
53 |
54 | ```
55 | POST /v1/posts HTTP/1.1
56 | ```
57 |
58 | ## Load Testing
59 |
60 | The best way to see what Play can do is to run a load test. We've included Gatling in this test project for integrated load testing.
61 |
62 | Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play scripts:
63 |
64 | ```
65 | sbt stage
66 | cd target/universal/stage
67 | bin/play-rest-api -Dplay.crypto.secret=testing
68 | ```
69 |
70 | Then you'll start the Gatling load test up (it's already integrated into the project):
71 |
72 | ```
73 | sbt gatling:test
74 | ```
75 |
76 | For best results, start the gatling load test up on another machine so you do not have contending resources. You can edit the [Gatling simulation](http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure), and change the numbers as appropriate.
77 |
78 | Once the test completes, you'll see an HTML file containing the load test chart:
79 |
80 | ```
81 | ./rest-api/target/gatling/gatlingspec-1472579540405/index.html
82 | ```
83 |
84 | That will contain your load test results.
85 |
--------------------------------------------------------------------------------
/app/ErrorHandler.scala:
--------------------------------------------------------------------------------
1 | import javax.inject.{Inject, Provider}
2 |
3 | import play.api._
4 | import play.api.http.DefaultHttpErrorHandler
5 | import play.api.http.Status._
6 | import play.api.libs.json.Json
7 | import play.api.mvc.Results._
8 | import play.api.mvc._
9 | import play.api.routing.Router
10 | import play.core.SourceMapper
11 |
12 | import scala.concurrent._
13 |
14 | /**
15 | * Provides a stripped down error handler that does not use HTML in error pages, and
16 | * prints out debugging output.
17 | *
18 | * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling
19 | */
20 | class ErrorHandler(environment: Environment,
21 | configuration: Configuration,
22 | sourceMapper: Option[SourceMapper] = None,
23 | optionRouter: => Option[Router] = None)
24 | extends DefaultHttpErrorHandler(environment,
25 | configuration,
26 | sourceMapper,
27 | optionRouter) {
28 |
29 | private val logger =
30 | org.slf4j.LoggerFactory.getLogger("application.ErrorHandler")
31 |
32 | // This maps through Guice so that the above constructor can call methods.
33 | @Inject
34 | def this(environment: Environment,
35 | configuration: Configuration,
36 | sourceMapper: OptionalSourceMapper,
37 | router: Provider[Router]) = {
38 | this(environment,
39 | configuration,
40 | sourceMapper.sourceMapper,
41 | Some(router.get))
42 | }
43 |
44 | override def onClientError(request: RequestHeader,
45 | statusCode: Int,
46 | message: String): Future[Result] = {
47 | logger.debug(
48 | s"onClientError: statusCode = $statusCode, uri = ${request.uri}, message = $message")
49 |
50 | Future.successful {
51 | val result = statusCode match {
52 | case BAD_REQUEST =>
53 | Results.BadRequest(message)
54 | case FORBIDDEN =>
55 | Results.Forbidden(message)
56 | case NOT_FOUND =>
57 | Results.NotFound(message)
58 | case clientError if statusCode >= 400 && statusCode < 500 =>
59 | Results.Status(statusCode)
60 | case nonClientError =>
61 | val msg =
62 | s"onClientError invoked with non client error status code $statusCode: $message"
63 | throw new IllegalArgumentException(msg)
64 | }
65 | result
66 | }
67 | }
68 |
69 | override protected def onDevServerError(
70 | request: RequestHeader,
71 | exception: UsefulException): Future[Result] = {
72 | Future.successful(
73 | InternalServerError(Json.obj("exception" -> exception.toString)))
74 | }
75 |
76 | override protected def onProdServerError(
77 | request: RequestHeader,
78 | exception: UsefulException): Future[Result] = {
79 | Future.successful(InternalServerError)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/docs/src/main/paradox/index.md:
--------------------------------------------------------------------------------
1 | # Making a REST API with Play
2 |
3 | This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com).
4 |
5 | We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways:
6 |
7 | * Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-rest-api](https://example.lightbend.com/v1/download/play-rest-api)
8 |
9 | * Linux/Mac:
10 | ```bash
11 | unzip play-rest-api.zip
12 | cd play-rest-api
13 | ./sbt
14 | ```
15 | * Windows:
16 |
17 | 1. Unzip the download
18 | 2. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run:
19 | ```
20 | sbt.bat
21 | ```
22 |
23 | * [From Github](http://github.com/playframework/play-rest-api):
24 | ```
25 | git clone https://github.com/playframework/play-rest-api.git
26 | ```
27 |
28 | This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [[appendix]].
29 |
30 | Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API.
31 |
32 | ## When to use Play
33 |
34 | Play makes a good REST API implementation because Play does the right thing out of the box. Play makes simple things easy, makes hard things possible, and encourages code that scales because it works in sympathy with the JVM and the underlying hardware. But "safe and does the right thing" is the boring answer.
35 |
36 | The fun answer is that [Play is **fast**](https://www.lightbend.com/blog/why-is-play-framework-so-fast).
37 |
38 | In fact, Play is so fast that you have to turn off machines so that the rest of your architecture can keep up. The Hootsuite team was able to **reduce the number of servers by 80%** by [switching to Play](https://www.lightbend.com/resources/case-studies-and-stories/how-hootsuite-modernized-its-url-shortener). if you deploy Play with the same infrastructure that you were using for other web frameworks, you are effectively staging a denial of service attack against your own database.
39 |
40 | Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play breaks network packets into a stream of small chunks of bytes. It keeps a small pool of work stealing threads, mapped to the number of cores in the machine, and keeps those threads fed with those chunks. Play exposes those byte chunks to the application for body parsing, Server Sent Events and WebSockets through [Akka Streams](http://doc.akka.io/docs/akka/2.4/scala/stream/stream-introduction.html) -- the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/).
41 |
42 | Linkedin uses Play throughout its infrastructure. It wins on all [four quadrants of scalability](http://www.slideshare.net/brikis98/the-play-framework-at-linkedin/128-Outline1_Getting_started_with_Play2) ([video](https://youtu.be/8z3h4Uv9YbE)). Play's average "request per second" comes in around [tens of k on a basic quad core w/o any intentional tuning](https://twitter.com/kevinbowling1/status/764188720140398592) -- and it only gets better.
43 |
44 | Play provides an easy to use MVC paradigm, including hot-reloading without any JVM bytecode magic or container overhead. Startup time for a developer on Play was **reduced by roughly 7 times** for [Walmart Canada](https://www.lightbend.com/resources/case-studies-and-stories/walmart-boosts-conversions-by-20-with-lightbend-reactive-platform), and using Play **reduced development times by 2x to 3x**.
45 |
46 | Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In both Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with [`java.util.concurrent.CompletionStage`](https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html) or [`scala.concurrent.Future`](http://docs.scala-lang.org/overviews/core/futures.html) API directly, and Play passes that asynchronous computation back through the framework.
47 |
48 | Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/esfand-r/play-java-dagger-dependency-injection#master), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play.
49 |
50 | ## Community
51 |
52 | To learn more about Play, check out the [Play tutorials](https://playframework.com/documentation/2.5.x/Tutorials) and see more examples and blog posts about Play, including streaming [Server Side Events](https://github.com/playframework/play-streaming-scala) and first class [WebSocket support](https://github.com/playframework/play-websocket-scala).
53 |
54 | To get more involved and if you have questions, join the [mailing list](https://groups.google.com/forum/#!forum/play-framework) at and follow [PlayFramework on Twitter](https://twitter.com/playframework).
55 |
56 | ## Microservices vs REST APIs
57 |
58 | One thing to note here is that although this guide covers how to make a REST API in Play, it only covers Play itself and deploying Play. Building a REST API in Play does not automatically make it a "microservice" because it does not cover larger scale concerns about microservices such as ensuring resiliency, consistency, or monitoring.
59 |
60 | For full scale microservices, you want [Lagom](http://www.lagomframework.com/), which builds on top of Play -- a microservices framework for dealing with the ["data on the outside"](https://blog.acolyer.org/2016/09/13/data-on-the-outside-versus-data-on-the-inside/) problem, set up with persistence and service APIs that ensure that the service always stays up and responsive even in the face of chaos monkeys and network partitions.
61 |
62 | With that caveat, let's start working with Play!
63 |
64 | @@@index
65 |
66 | * [Basics](part-1/index.md)
67 | * [Appendix](appendix.md)
68 |
69 | @@@
70 |
--------------------------------------------------------------------------------
/docs/src/main/paradox/part-1/index.md:
--------------------------------------------------------------------------------
1 | # Basics
2 |
3 | This guide will walk you through how to make a REST API with JSON using [Play 2.5](https://playframework.com).
4 |
5 | To see the associated Github project, please go to [https://github.com/playframework/play-rest-api](https://github.com/playframework/play-rest-api) or clone the project:
6 |
7 | ```
8 | git clone https://github.com/playframework/play-rest-api.git
9 | ```
10 |
11 | We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other.
12 |
13 | To run Play on your own local computer, please see the instructions in the [appendix](../appendix.md).
14 |
15 | ## Introduction
16 |
17 | We'll start off with a REST API that displays information for blog posts. Users should be able to write a title and a body of a blog post and create new blog posts, edit existing blog posts, and delete new blog posts.
18 |
19 | ## Modelling a Post Resource
20 |
21 | The way to do this in REST is to model the represented state as a resource. A blog post resource will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post.
22 |
23 | This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L13):
24 |
25 | ```scala
26 | case class PostResource(id: String, link: String,
27 | title: String, body: String)
28 | ```
29 |
30 | This resource is mapped to and from JSON on the front end using Play, and is mapped to and from a persistent datastore on the backend using a handler.
31 |
32 | Play handles HTTP routing and representation for the REST API and makes it easy to write a non-blocking, asynchronous API that is an order of magnitude more efficient than other web application frameworks.
33 |
34 | ## Routing Post Requests
35 |
36 | Play has two complimentary routing mechanisms. In the conf directory, there's a file called "routes" which contains entries for the HTTP method and a relative URL path, and points it at an action in a controller.
37 |
38 | ```
39 | GET / controllers.HomeController.index()
40 | ```
41 |
42 | This is useful for situations where a front end service is rendering HTML. However, Play also contains a more powerful routing DSL that we will use for the REST API.
43 |
44 | For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated `PostRouter` class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-rest-api/blob/master/conf/routes) file:
45 |
46 | ```
47 | -> /v1/posts v1.post.PostRouter
48 | ```
49 |
50 | The `PostRouter` examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala):
51 |
52 | ```scala
53 | package v1.post
54 | import javax.inject.Inject
55 |
56 | import play.api.mvc._
57 | import play.api.routing.Router.Routes
58 | import play.api.routing.SimpleRouter
59 | import play.api.routing.sird._
60 |
61 | class PostRouter @Inject()(controller: PostController)
62 | extends SimpleRouter
63 | {
64 | override def routes: Routes = {
65 | case GET(p"/") =>
66 | controller.index
67 |
68 | case POST(p"/") =>
69 | controller.process
70 |
71 | case GET(p"/$id") =>
72 | controller.show(id)
73 | }
74 | }
75 | ```
76 |
77 | Play’s [routing DSL](https://www.playframework.com/documentation/2.5.x/ScalaSirdRouter) (technically "String Interpolation Routing DSL", aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows:
78 |
79 | ```
80 | /posts/?sort=ascending&count=5
81 | ```
82 |
83 | then you can extract the "sort" and "count" parameters in a single line:
84 |
85 | ```scala
86 | GET("/" ? q_?"sort=$sort" & q_?”count=${ int(count) }")
87 | ```
88 |
89 | SIRD is especially useful in a REST API where there can be many possible query parameters. Cake Solutions covers SIRD in more depth in a [fantastic blog post](http://www.cakesolutions.net/teamblogs/all-you-need-to-know-about-plays-routing-dsl).
90 |
91 | ## Using a Controller
92 |
93 | The PostRouter has a PostController injected into it through standard [JSR-330 dependency injection](https://github.com/google/guice/wiki/JSR330) [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala#L12):
94 |
95 | ```scala
96 | class PostRouter @Inject()(controller: PostController)
97 | extends SimpleRouter
98 | ```
99 |
100 | Before heading into the PostController, let's discuss how controllers work in Play.
101 |
102 | A controller [handles the work of processing](https://www.playframework.com/documentation/2.5.x/ScalaActions) the HTTP request into an HTTP response in the context of an Action: it's where page rendering and HTML form processing happen. A controller extends [`play.api.mvc.Controller`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Controller), which contains a number of utility methods and constants for working with HTTP. In particular, a Controller contains Result objects such as Ok and Redirect, and HeaderNames like ACCEPT.
103 |
104 | The methods in a controller consist of a method returning an [Action](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Action). The Action provides the "engine" to Play.
105 |
106 | Using the action, the controller passes in a block of code that takes a [`Request`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Request) passed in as implicit – this means that any in-scope method that takes an implicit request as a parameter will use this request automatically. Then, the block must return either a [`Result`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Result), or a [`Future[Result]`](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future), depending on whether or not the action was called as `action { ... }` or [`action.async { ... }`](https://www.playframework.com/documentation/2.5.x/ScalaAsync#How-to-create-a-Future[Result]).
107 |
108 | ### Handling GET Requests
109 |
110 |
111 | Here's a simple example of a Controller:
112 |
113 | ```scala
114 | import javax.inject.Inject
115 | import play.api.mvc._
116 |
117 | import scala.concurrent._
118 |
119 | class MyController extends Controller {
120 |
121 | def index1: Action[AnyContent] = {
122 | Action { implicit request =>
123 | val r: Result = Ok("hello world")
124 | r
125 | }
126 | }
127 |
128 | def asyncIndex: Action[AnyContent] = {
129 | Action.async { implicit request =>
130 | val r: Future[Result] = Future.successful(Ok("hello world"))
131 | r
132 | }
133 | }
134 | }
135 | ```
136 |
137 | In this example, `index1` and `asyncIndex` have exactly the same behavior. Internally, it makes no difference whether we call `Result` or `Future[Result]` -- Play is non-blocking all the way through.
138 |
139 | However, if you're already working with `Future`, async makes it easier to pass that `Future` around. You can read more about this in the [handling asynchronous results](https://www.playframework.com/documentation/2.5.x/ScalaAsync) section of the Play documentation.
140 |
141 | The PostController methods dealing with GET requests is [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala):
142 |
143 | ```scala
144 | class PostController @Inject()(action: PostAction,
145 | handler: PostResourceHandler)
146 | (implicit ec: ExecutionContext)
147 | extends Controller {
148 |
149 | def index: Action[AnyContent] = {
150 | action.async { implicit request =>
151 | handler.find.map { posts =>
152 | Ok(Json.toJson(posts))
153 | }
154 | }
155 | }
156 |
157 | def show(id: String): Action[AnyContent] = {
158 | action.async { implicit request =>
159 | handler.lookup(id).map { post =>
160 | Ok(Json.toJson(post))
161 | }
162 | }
163 | }
164 |
165 | }
166 | ```
167 |
168 | Let's take `show` as an example. Here, the action defines a workflow for a request that maps to a single resource, i.e. `GET /v1/posts/123`.
169 |
170 | ```scala
171 | def show(id: String): Action[AnyContent] = {
172 | action.async { implicit request =>
173 | handler.lookup(id).map { post =>
174 | Ok(Json.toJson(post))
175 | }
176 | }
177 | }
178 | ```
179 |
180 | The id is passed in as a String, and the handler looks up and returns a `PostResource`. The `Ok()` sends back a `Result` with a status code of "200 OK", containing a response body consisting of the `PostResource` serialized as JSON.
181 |
182 | ### Processing Form Input
183 |
184 | Handling a POST request is also easy and is done through the `process` method:
185 |
186 | ```scala
187 | class PostController @Inject()(action: PostAction,
188 | handler: PostResourceHandler)
189 | (implicit ec: ExecutionContext)
190 | extends Controller {
191 |
192 | private val form: Form[PostFormInput] = {
193 | import play.api.data.Forms._
194 |
195 | Form(
196 | mapping(
197 | "title" -> nonEmptyText,
198 | "body" -> text
199 | )(PostFormInput.apply)(PostFormInput.unapply)
200 | )
201 | }
202 |
203 | def process: Action[AnyContent] = {
204 | action.async { implicit request =>
205 | processJsonPost()
206 | }
207 | }
208 |
209 | private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = {
210 | def failure(badForm: Form[PostFormInput]) = {
211 | Future.successful(BadRequest(badForm.errorsAsJson))
212 | }
213 |
214 | def success(input: PostFormInput) = {
215 | handler.create(input).map { post =>
216 | Created(Json.toJson(post))
217 | .withHeaders(LOCATION -> post.link)
218 | }
219 | }
220 |
221 | form.bindFromRequest().fold(failure, success)
222 | }
223 | }
224 | ```
225 |
226 | Here, the `process` action is an action wrapper, and `processJsonPost` does most of the work. In `processJsonPost`, we get to the [form processing](https://www.playframework.com/documentation/2.5.x/ScalaForms) part of the code.
227 |
228 | Here, `form.bindFromRequest()` will map input from the HTTP request to a [`play.api.data.Form`](https://www.playframework.com/documentation/2.5.x/api/scala/index.html#play.api.data.Form), and handles form validation and error reporting.
229 |
230 | If the `PostFormInput` passes validation, it's passed to the resource handler, using the `success` method. If the form processing fails, then the `failure` method is called and the `FormError` is returned in JSON format.
231 |
232 | ```scala
233 | private val form: Form[PostFormInput] = {
234 | import play.api.data.Forms._
235 |
236 | Form(
237 | mapping(
238 | "title" -> nonEmptyText,
239 | "body" -> text
240 | )(PostFormInput.apply)(PostFormInput.unapply)
241 | )
242 | }
243 | ```
244 |
245 | The form binds to the HTTP request using the names in the mapping -- "title" and "body" to the `PostFormInput` case class [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L11).
246 |
247 | ```scala
248 | case class PostFormInput(title: String, body: String)
249 | ```
250 |
251 | That's all you need to do to handle a basic web application! As with most things, there are more details that need to be handled. That's where creating custom Actions comes in.
252 |
253 | ## Using Actions
254 |
255 | We saw in the `PostController` that each method is connected to an Action through the "action.async" method [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L32):
256 |
257 | ```scala
258 | def index: Action[AnyContent] = {
259 | action.async { implicit request =>
260 | handler.find.map { posts =>
261 | Ok(Json.toJson(posts))
262 | }
263 | }
264 | }
265 | ```
266 |
267 | The action.async takes a function, and comes from the class parameter "action", which we can see is of type `PostAction` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L16):
268 |
269 | ```scala
270 | class PostController @Inject()(action: PostAction [...])
271 | ```
272 |
273 | `PostAction` is an ActionBuilder. It is involved in each action in the controller -- it mediates the paperwork involved with processing a request into a response, adding context to the request and enriching the response with headers and cookies. ActionBuilders are essential for handling authentication, authorization and monitoring functionality.
274 |
275 | ActionBuilders work through a process called [action composition](https://www.playframework.com/documentation/2.5.x/ScalaActionsComposition). The ActionBuilder class has a method called `invokeBlock` that takes in a `Request` and a function (also known as a block, lambda or closure) that accepts a `Request` of a given type, and produces a `Future[Result]`.
276 |
277 | So, if you want to work with an `Action` that has a "FooRequest" that has a Foo attached, it's easy:
278 |
279 | ```scala
280 | class FooRequest[A](request: Request[A], val foo: Foo) extends WrappedRequest(request)
281 |
282 | class FooAction extends ActionBuilder[FooRequest] {
283 | type FooRequestBlock[A] = FooRequest[A] => Future[Result]
284 |
285 | override def invokeBlock[A](request: Request[A], block: FooRequestBlock[A]) = {
286 | block(new FooRequest[A](request, new Foo))
287 | }
288 | }
289 | ```
290 |
291 | You create an `ActionBuilder[FooRequest]`, override `invokeBlock`, and then call the function with an instance of `FooRequest`.
292 |
293 | Then, when you call `fooAction`, the request type is `FooRequest`:
294 |
295 | ```scala
296 | fooAction { request: FooRequest =>
297 | Ok(request.foo.toString)
298 | }
299 | ```
300 |
301 | And `request.foo` will be added automatically.
302 |
303 | You can keep composing action builders inside each other, so you don't have to layer all the functionality in one single ActionBuilder, or you can create a custom `ActionBuilder` for each package you work with, according to your taste. For the purposes of this blog post, we'll keep everything together in a single class.
304 |
305 | You can see PostAction [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostAction.scala):
306 |
307 | ```scala
308 | class PostRequest[A](request: Request[A],
309 | val messages: Messages)
310 | extends WrappedRequest(request)
311 |
312 | class PostAction @Inject()(messagesApi: MessagesApi)
313 | (implicit ec: ExecutionContext)
314 | extends ActionBuilder[PostRequest] with HttpVerbs {
315 |
316 | type PostRequestBlock[A] = PostRequest[A] => Future[Result]
317 |
318 | private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass)
319 |
320 | override def invokeBlock[A](request: Request[A],
321 | block: PostRequestBlock[A]) = {
322 | if (logger.isTraceEnabled()) {
323 | logger.trace(s"invokeBlock: request = $request")
324 | }
325 |
326 | val messages = messagesApi.preferred(request)
327 | val future = block(new PostRequest(request, messages))
328 |
329 | future.map { result =>
330 | request.method match {
331 | case GET | HEAD =>
332 | result.withHeaders("Cache-Control" -> s"max-age: 100")
333 | case other =>
334 | result
335 | }
336 | }
337 | }
338 | }
339 | ```
340 |
341 | `PostAction` does a couple of different things here. The first thing it does is to log the request as it comes in. Next, it pulls out the localized `Messages` for the request, and adds that to a `PostRequest` , and runs the function, returning a `Future[Result]`.
342 |
343 | When the future completes, we map the result so we can replace it with a slightly different result. We compare the result's method against `HttpVerbs`, and if it's a GET or HEAD, we append a Cache-Control header with a max-age directive. We need an `ExecutionContext` for `future.map` operations, so we pass in the default execution context implicitly at the top of the class.
344 |
345 | Now that we have a `PostRequest`, we can call "request.messages" explicitly from any action in the controller, for free, and we can append information to the result after the user action has been completed.
346 |
347 | ## Converting resources with PostResourceHandler
348 |
349 | The `PostResourceHandler` is responsible for converting backend data from a repository into a `PostResource`. We won't go into detail on the `PostRepository` details for now, only that it returns data in an backend-centric state.
350 |
351 | A REST resource has information that a backend repository does not -- it knows about the operations available on the resource, and contains URI information that a single backend may not have. As such, we want to be able to change the representation that we use internally without changing the resource that we expose publicly.
352 |
353 | You can see the `PostResourceHandler` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala):
354 |
355 | ```scala
356 | class PostResourceHandler @Inject()(routerProvider: Provider[PostRouter],
357 | postRepository: PostRepository)
358 | (implicit ec: ExecutionContext)
359 | {
360 |
361 | def create(postInput: PostFormInput): Future[PostResource] = {
362 | val data = PostData(PostId("999"), postInput.title, postInput.body)
363 | postRepository.create(data).map { id =>
364 | createPostResource(data)
365 | }
366 | }
367 |
368 | def lookup(id: String): Future[Option[PostResource]] = {
369 | val postFuture = postRepository.get(PostId(id))
370 | postFuture.map { maybePostData =>
371 | maybePostData.map { postData =>
372 | createPostResource(postData)
373 | }
374 | }
375 | }
376 |
377 | def find: Future[Iterable[PostResource]] = {
378 | postRepository.list().map { postDataList =>
379 | postDataList.map(postData => createPostResource(postData))
380 | }
381 | }
382 |
383 | private def createPostResource(p: PostData): PostResource = {
384 | PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body)
385 | }
386 |
387 | }
388 | ```
389 |
390 | Here, it's a straight conversion in `createPostResource`, with the only hook being that the router provides the resource's URL, since it's something that `PostData` doesn't have itself.
391 |
392 | ## Rendering Content as JSON
393 |
394 | Play handles the work of converting a `PostResource` through [Play JSON](https://www.playframework.com/documentation/2.5.x/ScalaJson). Play JSON provides a DSL that looks up the conversion for the `PostResource` singleton object, so you don't need to declare it at the use point.
395 |
396 | You can see the `PostResource` object [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L18):
397 |
398 | ```scala
399 | object PostResource {
400 | implicit val implicitWrites = new Writes[PostResource] {
401 | def writes(post: PostResource): JsValue = {
402 | Json.obj(
403 | "id" -> post.id,
404 | "link" -> post.link,
405 | "title" -> post.title,
406 | "body" -> post.body)
407 | }
408 | }
409 | }
410 | ```
411 |
412 | Once the implicit is defined in the companion object, then it will be looked up automatically when handed an instance of the class. This means that when the controller converts to JSON, the conversion will just work, without any additional imports or setup.
413 |
414 | ```scala
415 | val json: JsValue = Json.toJson(post)
416 | ```
417 |
418 | Play JSON also has options to incrementally parse and generate JSON for continuously streaming JSON responses.
419 |
420 | ## Summary
421 |
422 | We've shown how to easy it is to put together a basic REST API in Play. Using this code, we can put together backend data, convert it to JSON and transfer it over HTTP with a minimum of fuss.
423 |
424 | In the next guide, we'll discuss content representation and provide an HTML interface that exists alongside the JSON API.
425 |
--------------------------------------------------------------------------------