play!ng with scala
DESCRIPTION
Play framework 2.0 presentation for Scala developers.TRANSCRIPT
ng with
Siarzh Miadzvedzeu @siarzh
2.0
is
“a web framework for a new era”
& “designed by web developers for web developers”
Guillaume Bort, creator
how it was0.x
how it was1.0
how it was1.2
how it was2.0
is
• full stack web framework for JVM• high-productive• asynch & reactive• stateless• HTTP-centric• typesafe• scalable• open source• part of Typesafe Stack 2.0
2.0
is fun and high-productive
• fast turnaround: change you code and hit reload! :)• browser error reporting• db evolutions• modular and extensible via plugins• minimal bootstrap• integrated test framework• easy cloud deployment (e.g. Heroku)
2.0
is asynch & reactive
• WebSockets• Comet• HTTP 1.1 chuncked responses• composable streams handling • based on event-driven, non-blocking Iteratee I/O
2.0
is HTTP-centric
• based on HTTP and stateless • doesn't fight HTTP or browser• clean & easy URL design (via routes)• designed to work with HTML5
2.0
“When a web framework starts an architecture fight with the web, the framework loses.”
is typesafe where it matters
• templates• routes• configs• javascript (via Goggle Closure)
• LESS stylesheets• CoffeeScript
2.0
all are compiled }+ browser error reporting
getting started2.0
3. create new project:
4. run the project:
export PATH=$PATH:/path/to/play20
1. download Play 2.0 binary package
2. add play to your PATH:
$ play new myFirstApp
$ cd myFirstApp $ play run
config2.0# This is the main configuration file for the application.# The application languages# ~~~~~application.langs="en" # Database configuration# ~~~~~# You can declare as many datasources as you want.# By convention, the default datasource is named `default`#db.default.driver=org.h2.Driverdb.default.url="jdbc:h2:mem:play"# db.default.user=sa# db.default.password= # Evolutions# ~~~~~# You can disable evolutions if needed# evolutionplugin=disabled...
single conf/application.conf:
IDE support2.0 $ eclipsify
$ idea
$ play netbeans
resolvers += {
"remeniuk repo" at "http://remeniuk.github.com/maven" }
libraryDependencies += { "org.netbeans" %% "sbt-netbeans-plugin" % "0.1.4"}
Eclipse:
IntelliJ IDEA:
Netbeans: add to plugins.sbt:
and SBT2.0 plugins.sbtBuild.scala
import sbt._import Keys._import PlayProject._ object ApplicationBuild extends Build { val appName = "Your application" val appVersion = "1.0" val appDependencies = Seq( // Add your project dependencies here, ) val main = PlayProject( appName, appVersion, appDependencies, mainLang = SCALA ).settings( // Add your own project settings here ) }
addSbtPlugin("play" % "sbt-plugin" % "2.0")
routes2.0# Home page
GET / controllers.Application.homePage()GET /home controllers.Application.show(page = "home")
# Display a client.GET /clients/all controllers.Clients.list()GET /clients/:id controllers.Clients.show(id: Long)
# Pagination links, like /clients?page=3GET /clients controllers.Clients.list(page: Int ?= 1)
# With regexGET /orders/$id<[0-9]+> controllers.Orders.show(id: Long)
# 'name' is all the rest part of the url including '/' symbolsGET /files/*name controllers.Application.download(name)
reversed routing2.0# Hello actionGET /helloBob controllers.Application.helloBobGET /hello/:name controllers.Application.hello(name)
// Redirect to /hello/Bobdef helloBob = Action { Redirect( routes.Application.hello("Bob") ) }
actions2.0Action(parse.text) { request => Ok("<h1>Got: " + request.body + "</h1>").as(HTML).withSession( session + ("saidHello" -> "yes")
).withHeaders( CACHE_CONTROL -> "max-age=3600", ETAG -> "xx" ).withCookies( Cookie("theme", "blue") )}
action: (play.api.mvc.Request => play.api.mvc.Result)
val notFound = NotFoundval pageNotFound = NotFound(<h1>Page not found</h1>)val badRequest = BadRequest(views.html.form(formWithErrors))val oops = InternalServerError("Oops")val anyStatus = Status(488)("Strange response type")
controllers2.0package controllersimport play.api.mvc._
object Application extends Controller {
def index = Action { Ok("It works!") }
def hello(name: String) = Action { Ok("Hello " + name) }
def goodbye(name: String) = TODO }
templates2.0 @(title: String)(content: Html)
<!DOCTYPE html> <html> <head> <title>@title</title> </head> <body> <section class="content">@content</section> </body> </html>
@(name: String = “Guest”)
@main(title = "Home") { <h1>Welcome @name! </h1> }
views/main.scala.html:
views/hello.scala.html:
…are just functions ;)
val html = views.html.Application.hello(name) then from Scala class:
database evolutions2.0# Add Post # --- !UpsCREATE TABLE Post ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, content text NOT NULL, postedAt date NOT NULL, author_id bigint(20) NOT NULL, FOREIGN KEY (author_id) REFERENCES User(id), PRIMARY KEY (id)); # --- !DownsDROP TABLE Post;
conf/evolutions/${x}.sql:
browser error reporting2.0
access SQL data via Anorm2.0
// using Parser API import anorm.SqlParser._
val count: Long = SQL("select count(*) from Country").as(scalar[Long].single)
val result:List[String~Int] = { SQL("select * from Country") .as(get[String]("name")~get[Int]("population") map { case n~p => (n,p) } *) }
import anorm._
DB.withConnection { implicit c =>
val selectCountries = SQL("Select * from Country") // Transform the resulting Stream[Row] to a List[(String,String)] val countries = selectCountries().map(row => row[String]("code") -> row[String]("name") ).toList }
and Akka2.0
// schedule sending message 'tick' to testActor every 30 minutesAkka.system.scheduler.schedule(0 seconds, 30 minutes, testActor, "tick")
// schedule a single taskAkka.system.scheduler.scheduleOnce(10 seconds) { file.delete()}
def index = Action { // using actors, coverting Akka Future to Play Promise Async {
(myActor ? "hello").mapTo[String].asPromise.map { response => Ok(response) } }}
def index = Action { // execute some task asynchronously Async {
Akka.future { longComputation() }.map { result => Ok("Got " + result) } }}
streaming response2.0
// Play has a helper for the above:
def index = Action {
val file = new java.io.File("/tmp/fileToServe.pdf") val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile(file) SimpleResult( header = ResponseHeader(200, Map(CONTENT_LENGTH -> file.length.toString)),
body = fileContent )}
def index = Action { Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))}
chunked results2.0
// Play has a helper for the ChunkedResult above:
def index = Action {
val data = getDataStream val dataContent: Enumerator[Array[Byte]] = Enumerator.fromStream(data) ChunkedResult( header = ResponseHeader(200),
chunks = dataContent )}
Ok.stream(dataContent)
Comet2.0lazy val clock: Enumerator[String] = {
val dateFormat = new SimpleDateFormat("HH mm ss") Enumerator.fromCallback { () => Promise.timeout(Some(dateFormat.format(new Date)), 100 milliseconds) } } def liveClock = Action { Ok.stream(clock &> Comet(callback = "parent.clockChanged")) }
and then in template:
<script type="text/javascript" charset="utf-8"> var clockChanged = function(time) { // do something }</script> <iframe id="comet" src="@routes.Application.liveClock.unique"></iframe>
WebSockets2.0
def index = WebSocket.using[String] { request => // Log events to the console val in = Iteratee.foreach[String](println).mapDone { _ => println("Disconnected") } // Send a single 'Hello!' message val out = Enumerator("Hello!") (in, out)}
just another action in controller:
caching API2.0 by default uses EHCache, configurable via plugins
Cache.set("item.key", connectedUser)
val user: User = Cache.getOrElse[User]("item.key") { User.findById(connectedUser)}
// cache HTTP responsedef index = Cached("homePage",600) { Action { Ok("Hello world") }}
i18n2.0
application.langs="en,en-US,fr"
home.title=File viewer
files.summary=The disk {1} contains {0} file(s).
conf/application.conf:
conf/messages.en:
val title = Messages("home.title") val titleFR = Messages("home.title")(Lang(“fr")) val summary = Messages("files.summary", d.files.length, d.name)
from Scala class:
from template: <h1>@Messages("home.title")</h1>
testing2.0 …using specs2 by default
"Computer model" should {
"be retrieved by id" in { running(FakeApplication(additionalConfiguration = inMemoryDatabase())) { val Some(macintosh) = Computer.findById(21)
macintosh.name must equalTo("Macintosh") macintosh.introduced must beSome.which(dateIs(_, "1984-01-24")) } }}
testing templates2.0
"render index template" in { val html = views.html.index("Coco") contentType(html) must equalTo("text/html") contentAsString(html) must contain("Hello Coco")}
testing controllers2.0
"respond to the index Action" in { val result = controllers.Application.index("Bob")(FakeRequest()) status(result) must equalTo(OK) contentType(result) must beSome("text/html") charset(result) must beSome("utf-8") contentAsString(result) must contain("Hello Bob")}
testing routes2.0
"respond to the index Action" in { val Some(result) = routeAndCall(FakeRequest(GET, "/Bob")) status(result) must equalTo(OK) contentType(result) must beSome("text/html") charset(result) must beSome("utf-8") contentAsString(result) must contain("Hello Bob")}
testing server2.0
"run in a server" in { running(TestServer(3333)) { await( WS.url("http://localhost:3333").get ).status must equalTo(OK) }}
testing with browser2.0
"run in a browser" in {
running(TestServer(3333), HTMLUNIT) { browser => browser.goTo("http://localhost:3333") browser.$("#title").getTexts().get(0) must equalTo("Hello Guest") browser.$("a").click() browser.url must equalTo("http://localhost:3333/Coco") browser.$("#title").getTexts().get(0) must equalTo("Hello Coco")
}}
…using Selenium WebDriver with FluentLenium
Demo2.0
Resources2.0http://www.playframework.org/https://github.com/playframework/Play20/wikihttp://www.parleys.com/#st=5&id=3143&sl=4http://www.parleys.com/#st=5&id=3144&sl=13http://www.parleys.com/#id=3081&st=5http://vimeo.com/41094673
Questions2.0 ?