第2回ではsprayのテンプレート・プロジェクトをデバッグ実行してみました。
今回は、かんたんなTODO登録を作成してみます。
掲載したコードはGithubにて公開しています。
https://github.com/x1-/spray-sandbox
1. パッケージ名の変更
まずは・・・sprayのテンプレートは、ソース・パッケージがcom.exampleのままなので適切なパッケージ名に変更しておきます。
ソース・パッケージ上でShift+F6押すとrenameダイアログが立ち上がります。
私はcom.inkenkun.x1に変更しました。
build.sbtの定義も変更します。
organization := "com.inkenkun.x1" version := "0.1" scalaVersion := "2.11.2" scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8") resolvers += "repo.typesafe.com" at "http://repo.typesafe.com/typesafe/releases/" libraryDependencies ++= { val akkaV = "2.3.6" val sprayV = "1.3.2" Seq( "io.spray" %% "spray-can" % sprayV, "io.spray" %% "spray-routing" % sprayV, "io.spray" %% "spray-testkit" % sprayV % "test", "com.typesafe.akka" %% "akka-actor" % akkaV, "com.typesafe.akka" %% "akka-testkit" % akkaV % "test", "org.specs2" %% "specs2-core" % "2.3.11" % "test" ) } Revolver.settings
2. TODO登録ページをつくる
MyService.scalaを開きます。
package com.inkenkun.x1 import akka.actor.Actor import spray.routing._ import spray.http._ import MediaTypes._ // we don't implement our route structure directly in the service actor because // we want to be able to test it independently, without having to spin up an actor class MyServiceActor extends Actor with MyService { // the HttpService trait defines only one abstract member, which // connects the services environment to the enclosing actor or test def actorRefFactory = context // this actor only runs our route, but you could add // other things here, like request stream processing // or timeout handling def receive = runRoute( myRoute ) } // this trait defines our service behavior independently from the service actor trait MyService extends HttpService { val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete { <html> <body> <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1> </body> </html> } } } } }
ルーティングにTODO登録ページを追加します。
val myRoute =…に“add”というパスを追加します。
val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete { <html> <body> <h1>Say hello to <i>spray-routing</i> on <i>spray-can</i>!</h1> </body> </html> } } } } ~ path( "add" ) { get { respondWithMediaType( `text/html` ) { complete { <html> <body> </body> </html> } } } }
“add”ページはパラメータを取らないので、pathを使います。
HTMLを返却するのでrespondWithMediaType( `text/html` )を追加しました。
ここで・・・
completeの中に直接HTML文字列を記述しているのに、エラーになりません。
これはsprayもakkaも関係ありません。scalaの機能です。
scalaではXMLをプログラム中に直接書くことができるのです。
※詳しくは、hishidamaさんのScala XMLをご参照ください。
ここまでで実行します。
無事起動したらブラウザでhttp://localhost:8080/addを開きます。
TODO入力ボックスが表示されます。
さて、せっかくなのでトップページのHTMLを編集して”add”へのリンクを作成しておきます。
val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete { <html> <body> <h1>TODO管理 |д゚)チラッ</h1> <ul> <li><a href="/add" title="TODO登録">TODO登録</a></li> </ul> </body> </html> } } } } ~ :
またルーティングにHTMLを記述してしまうと、変更しづらいので外に出します。
trait MyService extends HttpService { val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete ( index ) } } } ~ path( "add" ) { get { respondWithMediaType( `text/html` ) { complete ( add ) } } } lazy val index = <html> <body> <h1>TODO管理 |д゚)チラッ</h1> <ul> <li><a href="/add" title="TODO登録">TODO登録</a></li> </ul> </body> </html> lazy val add = <html> <body> <h2>TODO登録ヾ(*´∀`*)ノ</h2> <form name="form1" method="POST" action="add"> <div> <span><input type="text" name="todo" value="" style="width:60%;" /></span> <span><input type="submit" name="submit" value="登録" /></span> </div> </form> </body> </html> }
3. 入力されたTODOを保存する
入力されたTODOを保存します。
とりあえず、現時点ではキャッシュに保存することにします。
※永続化は後ほど・・・
まずはMyService.scalaにキャッシュオブジェクトを作成しておきます。
object Todos { private var items: Seq[String] = Seq() def add( item: String ): Unit = synchronized { items = items :+ item } def remove( item: String ): Unit = { val ( head, tail ) = items.span( _ != item ) synchronized { if ( tail.size == 0 ) head else head ++ tail.tail } } def all: Seq[String] = items }
今度はルーティングにPOSTを追加します。
val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete ( index ) } } } ~ path( "add" ) { get { respondWithMediaType( `text/html` ) { complete ( add ) } } ~ post { respondWithMediaType( `text/html` ) { complete ( add ) } } }
これだけだと、addページと同じものを返すだけなので、POSTされた値を取り出します。
post { formFields( 'todo ) { todo => respondWithMediaType( `text/html` ) { complete ( add ) } } }
これで、formの中のtodoフィールドを取り出せました。
todoに入力された値をキャッシュオブジェクトに追加します。
post { formFields( 'todo ) { todo => Todos.add( todo ) respondWithMediaType( `text/html` ) { complete ( add ) } } }
lazy val add = <html>…も少し改造して、入力された値を表示するようにしましょう。
lazy valをdefに変更して引数を取るようにします。
def add( items: Seq[String] = Seq.empty[String] ) = <html> <body> <h2>TODO登録ヾ(*´∀`*)ノ</h2> <form name="form1" method="POST" action="add"> <div> <span><input type="text" name="todo" value="" style="width:60%;" /></span> <span><input type="submit" name="submit" value="登録" /></span> </div> </form> <div> <ul> { for { item <- items } yield <li>{item}</li> } </ul> </div> </body> </html>
ルーティングの方も変更します。
path( "add" ) { get { respondWithMediaType( `text/html` ) { complete ( add( Todos.all ) ) } } ~ post { formFields( 'todo ) { todo => Todos.add( todo ) respondWithMediaType( `text/html` ) { complete ( add( Todos.all ) ) } } } }
Response返却の部分がGETとPOSTで被っているのでリファクタしておきます。
val myRoute = path( "" ) { get { respondWithMediaType( `text/html` ) { // XML is marshalled to `text/xml` by default, so we simply override here complete ( index ) } } } ~ path( "add" ) { get { addPage } ~ post { formFields( 'todo ) { todo => Todos.add( todo ) addPage } } } def addPage: Route = { respondWithMediaType( `text/html` ) { complete( add( Todos.all ) ) } }
ここまで出来たら実行してみます。
TODOを登録すると・・・
下に追加されますv(´∀`*v)ピース
今回はHTMLを返すTODO登録ページを作成しましたが、次回はJSONを送受信するREST APIを作成したいと思います。