Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Play2 Up & Running

Play2 Up & Running

Presented at the Play Framework NYC meetup, this is a guided introduction to building a web application with Play 2.

The finished code is available here https://github.com/play-nyc/play-gilt

63e2f6b0de6ae817af2e185b82aa05c2?s=128

Mark Wunsch

April 17, 2013
Tweet

Transcript

  1. up & running Mark Wunsch @markwunsch

  2. None
  3. Play is not a Java framework.

  4. None
  5. Let’s make an app!

  6. •I won’t use a Database •I will use Scala two

    things
  7. dev.gilt.com

  8. building blocks • `play new my-app` • `play run` •

    play.api.mvc.Controller • play.api.mvc.Action • play.api.libs.ws.WS • play.api.Configuration • scala.concurrent.Future • play.api.libs.json.Json • play.api.mvc.AsyncResult • play.api.templates.Html • controllers.Assets • play.api.test.Helpers • `play clean compile stage`
  9. None
  10. mark:~/Projects $ play new my-app _ _ _ __ |

    | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.1 (using Java 1.6.0_43 and Scala 2.10.0), http://www.playframework.org The new application will be created in /Users/mwunsch/Projects/my-app What is the application name? [my-app] > my-app Which template do you want to use for this new application? 1 - Create a simple Scala application 2 - Create a simple Java application > 1 OK, application my-app is created. Have fun!
  11. mark:~/Projects/my-app $ play [info] Loading global plugins from /Users/mwunsch/.sbt/plugins [info]

    Loading project definition from /Users/mwunsch/Projects/my-app/project [info] Set current project to my-app (in build file:/Users/mwunsch/Projects/my-app/) _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.1 (using Java 1.6.0_43 and Scala 2.10.0), http://www.playframework.org > Type "help play" or "license" for more information. > Type "exit" or use Ctrl+D to leave this console. [my-app] $
  12. “simple” build tool http://www.scala-sbt.org/

  13. [my-app] $ help play Welcome to Play 2.1.1! These commands

    are available: ----------------------------- classpath Display the project classpath. clean Clean all generated files. compile Compile the current application. console Launch the interactive Scala console (use :quit to exit). dependencies Display the dependencies summary. dist Construct standalone application package. exit Exit the console. h2-browser Launch the H2 Web browser. license Display licensing informations. package Package your application as a JAR. play-version Display the Play version. publish Publish your application in a remote repository. publish-local Publish your application in the local repository. reload Reload the current application build file. run <port> Run the current application in DEV mode. test Run Junit tests and/or Specs from the command line eclipse generate eclipse project file idea generate Intellij IDEA project file sh <command to run> execute a shell command start <port> Start the current application in another JVM in PROD mode. update Update application dependencies. Type `help` to get the standard sbt help.
  14. [my-app] $ run [info] Updating {file:/Users/mwunsch/Projects/my-app/}my-app... [info] Done updating. ---

    (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0%0:9000 (Server started, use Ctrl+D to stop and go back to the console...)
  15. None
  16. •stores (women, men, baby & kids, home) •sales •products gilt

    api
  17. mark:~/Projects/my-app $ tree. . !"" README !"" app # !""

    controllers # # $"" Application.scala # $"" views # !"" index.scala.html # $"" main.scala.html !"" conf # !"" application.conf # $"" routes !"" logs # $"" application.log !"" project # !"" Build.scala # !"" build.properties # !"" plugins.sbt !"" public # !"" images # # $"" favicon.png # !"" javascripts # # $"" jquery-1.9.0.min.js # $"" stylesheets # $"" main.css !"" target $"" test !"" ApplicationSpec.scala $"" IntegrationSpec.scala
  18. # Routes # This file defines all application routes (Higher

    priority routes first) # ~~~~ # Get all active sales GET / controllers.Application.index # Get active sales by store GET /:store controllers.Stores.activeSalesByKey(store) # Get a particular sale GET /:store/:sale controllers.Sales.show(sale, store) # Get a particular product GET /product/:id controllers.Products.show(id: Long) # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file)
  19. package controllers import play.api._ import play.api.mvc._ object Stores extends Controller

    { def activeSalesByKey(store: String) = TODO }
  20. val TODO = Action { NotImplemented[play.api.templates.Html]( views.html.defaultpages.todo() ) } trait

    Action[A] extends EssentialAction { def apply(request: Request[A]): Result }
  21. None
  22. package test import org.specs2.mutable._ import play.api.test._ import play.api.test.Helpers._ class StoresSpec

    extends Specification { "Stores#activeSalesByKey" should { "be successful" in { val res = controllers.Stores.activeSalesByKey("women")(FakeRequest()) status(res) must equalTo(OK) } } }
  23. [my-app] $ test [info] StoresSpec [info] [info] Stores#activeSalesByKey should [error]

    x be successful [error] '501' is not equal to '200' (StoresSpec.scala:14) [info] [info] [info] Total for specification StoresSpec [info] Finished in 73 ms [info] 1 example, 1 failure, 0 error
  24. ... import scala.concurrent.Future import play.api.libs._ import play.api.libs.concurrent.Execution.Implicits._ object Stores extends

    Controller { def activeSalesByKey(store: String) = Action { val eventualResponse: Future[ws.Response] = { ws.WS.url(API_ROOT+"sales/"+store+"/active.json").get() } Async { eventualResponse map { response => Logger.info("API Response: %s %s".format(response.status, response.statusText)) Status(response.status) { response.body } } } } private val API_ROOT = "https://api.gilt.com/v1/" }
  25. [info] application - API Response: 401 Unauthorized

  26. ... object Stores extends Controller { def activeSalesByKey(store: String) =

    Action { val eventualResponse: Future[ws.Response] = { signedApiRequest("sales/"+store+"/active.json").get() } Async { eventualResponse map { response => Status(response.status) { response.body } } } } private def signedApiRequest(store: String) = { val apiKey = Play.maybeApplication flatMap { app => app.configuration.getString("gilt.apikey") } getOrElse "" ws.WS.url(API_ROOT+path).withQueryString("apikey" -> apiKey) } ... }
  27. # In conf/application.conf # Gilt API key, found in the

    $GILT_API_KEY env var. gilt.apikey=${?GILT_API_KEY}
  28. mark:~/Projects/my-app $ export GILT_API_KEY=‘redacted’ mark:~/Projects/my-app $ play

  29. mark:~/Projects/my-app $ curl localhost:9000/women {"sales":[{"name":"Wardrobe Essentials: Everything You Need","sale":"https://api.gilt.com/v1/sales/women/wardrobe- essentials-413/detail.json","sale_key":"wardrobe-

    essentials-413","store":"women","sale_url":"http://www.gilt.com/ sale/women/wardrobe-essentials-413/ss? utm_medium=api&utm_campaign=play- tutorial&utm_source=salesapi","image_urls":{"744x281": [{"url":"http://cdn1.gilt.com/images/share/uploads/ 0000/0002/0068/200686017/orig.jpg ...
  30. class StoresSpec extends Specification { "Stores#activeSalesByKey" should { "be successful"

    in { running(FakeApplication()) { val res = controllers.Stores.activeSalesByKey("women")(FakeRequest()) status(res) must equalTo(OK) } } } }
  31. [my-app] $ test [info] StoresSpec [info] [info] Stores#activeSalesByKey should [info]

    + be successful [info] [info] [info] Total for specification StoresSpec [info] Finished in 324 ms [info] 1 example, 0 failure, 0 error
  32. building blocks ✓ `play new my-app` ✓ `play run` ✓

    play.api.mvc.Controller ✓ play.api.mvc.Action ✓ play.api.libs.ws.WS ✓ play.api.Configuration ✓ scala.concurrent.Future • play.api.libs.json.Json ✓ play.api.mvc.AsyncResult • play.api.templates.Html • controllers.Assets ✓ play.api.test.Helpers • `play clean compile stage`
  33. package models case class Sale( name: String, key: String, giltUrl:

    String, apiUrl: String )
  34. import play.api.libs.json._ object Stores extends Controller { def activeSalesByKey(store: String)

    = Action { ... Async { eventualResponse map { response => response.status match { case OK => Ok( unmarshalSales(response.json).toString ) case _ => Status(response.status) { response.body } } } } } private def unmarshalSales(json: JsValue): Seq(model.Sale) = { json.as[Seq[Sale]] } ... }
  35. [my-app] $ compile [info] Compiling 1 Scala source to /Users/mwunsch/Projects/play-

    gilt/target/scala-2.10/classes... [error] /Users/mwunsch/Projects/play-gilt/app/controllers/ Stores.scala:29: No Json deserializer found for type Seq[models.Sale]. Try to implement an implicit Reads or Format for this type. [error] json.as[Seq[models.Sale]] [error] ^ [error] one error found [error] (compile:compile) Compilation failed [error] Total time: 1 s, completed Apr 17, 2013 10:46:58 AM
  36. package models import play.api.libs.json._ case class Sale( name: String, key:

    String, giltUrl: String, apiUrl: String ) object Sale { implicit val readSale: Reads[Sale] = new Reads[Sale] { def reads(json: JsValue): JsResult[Sale] = { for { name <- (json \ "name").validate[String] key <- (json \ "sale_key").validate[String] url <- (json \ "sale_url").validate[String] api <- (json \ "sale").validate[String] } yield Sale(name, key, url, api) } } }
  37. ... import models.Sale object Stores extends Controller { ... private

    def unmarshalSales(json: JsValue): Seq(Sale) = { (json \ "sales").validate[Seq[Sale]].fold( err => Nil, success => success ) } ... }
  38. None
  39. ... object Stores extends Controller { def activeSalesByKey(store: String) =

    Action { ... Async { eventualResponse map { response => response.status match { case OK => { val sales = unmarshalSales(response.json) Ok( views.html.store(sales, store) ) } case _ => Status(response.status) { response.body } } } } } ... }
  40. @(sales: Seq[Sale], store: String) @main(store) { <ul class="active-sales"> @for(sale <-

    sales) { <li class="sale" id="@sale.key"> <a href="@sale.giltUrl" class="url">@sale.name</a> </li> } </ul> }
  41. mark:~/Projects/my-app $ tree app/assets app/assets $"" stylesheets $"" sales.less

  42. <link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/ sales.css")">

  43. None
  44. deploying to production

  45. mark:~/Projects/my-app $ heroku create Creating whispering-sierra-2474... done, stack is cedar

    http://whispering-sierra-2474.herokuapp.com/ | git@heroku.com:whispering-sierra-2474.git Git remote heroku added mark:~/Projects/my-app $ git push heroku master Counting objects: 79, done. Delta compression using up to 8 threads. Compressing objects: 100% (70/70), done. Writing objects: 100% (79/79), 42.72 KiB, done. Total 79 (delta 22), reused 0 (delta 0) -----> Play 2.x - Scala app detected -----> Installing OpenJDK 1.6...done -----> Building app with sbt -----> Running: sbt clean compile stage
  46. mark:~/Projects/my-app $ heroku config:set GILT_API_KEY='redacted' Setting config vars and restarting

    whispering-sierra-2474... done, v7 GILT_API_KEY: redacted
  47. http://play-gilt.herokuapp.com/women

  48. https://github.com/play-nyc/play-gilt

  49. building blocks ✓ `play new my-app` ✓ `play run` ✓

    play.api.mvc.Controller ✓ play.api.mvc.Action ✓ play.api.libs.ws.WS ✓ play.api.Configuration ✓ scala.concurrent.Future ✓ play.api.libs.json.Json ✓ play.api.mvc.AsyncResult ✓ play.api.templates.Html ✓ controllers.Assets ✓ play.api.test.Helpers ✓ `play clean compile stage`
  50. None