sprayでwebサービス作るチュートリアル 第3回

2014-11-08

第2回ではsprayのテンプレート・プロジェクトをデバッグ実行してみました。
今回は、かんたんなTODO登録を作成してみます。

掲載したコードはGithubにて公開しています。
https://github.com/x1-/spray-sandbox

1. パッケージ名の変更

まずは・・・sprayのテンプレートは、ソース・パッケージがcom.exampleのままなので適切なパッケージ名に変更しておきます。

ソース・パッケージ上でShift+F6押すとrenameダイアログが立ち上がります。

ij8

私は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入力ボックスが表示されます。

スクリーンショット 2014-11-06 12.35.33

さて、せっかくなのでトップページの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 valdefに変更して引数を取るようにします。

  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 ) )
    }
  }

ここまで出来たら実行してみます。

スクリーンショット 2014-11-08 16.23.25

TODOを登録すると・・・

スクリーンショット 2014-11-08 16.24.33

下に追加されますv(´∀`*v)ピース

スクリーンショット 2014-11-08 16.24.44

今回はHTMLを返すTODO登録ページを作成しましたが、次回はJSONを送受信するREST APIを作成したいと思います。