├── 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 | --------------------------------------------------------------------------------