RESTful APIs

XitrumではiPhone、Androidなどのアプリケーション用のRESTful APIsを非常に簡単に記述することができます。

import xitrum.Action
import xitrum.annotation.GET

@GET("articles")
class ArticlesIndex extends Action {
  def execute() {...}
}

@GET("articles/:id")
class ArticlesShow extends Action {
  def execute() {...}
}

POST、 PUT、 PATCH、 DELETEそしてOPTIONSと同様に XitrumはHEADリクエストをボディが空のGETリクエストとして自動的に扱います。

通常のブラウザーのようにPUTとDELETEをサポートしていないHTTPクライアントにおいて、 PUTとDELETEを実現するには、リクエストボディに _method=put や、 _method=delete を含めることで 可能になります。

アプリケーションの起動時にXitrumはアプリケーションをスキャンし、ルーティングテーブルを作成し出力します。 以下の様なログからアプリケーションがどのようなAPIをサポートしているか知ることができます。

[INFO] Routes:
GET /articles     quickstart.action.ArticlesIndex
GET /articles/:id quickstart.action.ArticlesShow

ルーティングはJAX-RSとRailsエンジンの思想に基づいて自動で収集されます。 全てのルートを1箇所に宣言する必要はありません。 この機能は分散ルーティングと捉えることができます。この機能のおかげでアプリケーションを他のアプリケーションに取り込むことが可能になります。 もしあなたがブログエンジンを作ったならそれをJARにして別のアプリケーションに取り込むだけですぐにブログ機能が使えるようになるでしょう。 ルーティングには更に2つの特徴があります。 ルートの作成(リバースルーティング)は型安全に実施され、 Swagger Doc を使用したルーティングに関するドキュメント作成も可能となります。

ルートのキャッシング

起動スピード改善のため、ルートは routes.cache ファイルにキャッシュされます。 開発時には target にあるクラスファイル内のルートはキャッシュされません。 もしルートを含む依存ライブラリを更新した場合、 routes.cache ファイルを削除してください。 また、このファイルはソースコードリポジトリにコミットしないよう気をつけましょう。

ルートの優先順位(first、last)

以下の様なルートを作成した場合

/articles/:id --> ArticlesShow
/articles/new --> ArticlesNew

2番目のルートを優先させるには @First アノテーションを追加します。

import xitrum.annotation.{GET, First}

@GET("articles/:id")
class ArticlesShow extends Action {
  def execute() {...}
}

@First  // This route has higher priority than "ArticlesShow" above
@GET("articles/new")
class ArticlesNew extends Action {
  def execute() {...}
}

Last も同じように使用できます。

Actionへの複数パスの関連付け

@GET("image", "image/:format")
class Image extends Action {
  def execute() {
    val format = paramo("format").getOrElse("png")
    // ...
  }
}

ドットを含むルート

@GET("articles/:id", "articles/:id.:format")
class ArticlesShow extends Action {
  def execute() {
    val id     = param[Int]("id")
    val format = paramo("format").getOrElse("html")
    // ...
  }
}

正規表現によるルーティング

ルーティングに正規表現を使用することも可能です。

GET("articles/:id<[0-9]+>")

パスの残り部分の取得

/ 文字が特別でパラメータ名に含まれられません。/ 文字を使いたい場合、以下のように書きます:

GET("service/:id/proxy/:*")

以下のパスがマッチされます:

/service/123/proxy/http://foo.com/bar

:* を取得:

val url = param("*")  // "http://foo.com/bar"となります

アクションへのリンク

Xitrumは型安全指向です。URLは直截記載せずにいかのように参照します:

<a href={url[ArticlesShow]("id" -> myArticle.id)}>{myArticle.title}</a>

他のアクションへのリダイレクト

redirectTo[AnotherAction]() を使用します。 リダイレクトについては こちら(英語) を参照してください。

import xitrum.Action
import xitrum.annotation.{GET, POST}

@GET("login")
class LoginInput extends Action {
  def execute() {...}
}

@POST("login")
class DoLogin extends Action {
  def execute() {
    ...
    // After login success
    redirectTo[AdminIndex]()
  }
}

GET("admin")
class AdminIndex extends Action {
  def execute() {
    ...
    // Check if the user has not logged in, redirect him to the login page
    redirectTo[LoginInput]()
  }
}

また、redirecToThis() を使用して現在のアクションへリダイレクトさせることも可能です。

他のアクションへのフォワード

forwardTo[AnotherAction]() を使用します。前述の redirectTo ではブラウザは別のリクエストを送信しますが、 forwardTo ではリクエストは引き継がれます。

Ajaxリクエストの判定

isAjax を使用します。

// In an action
val msg = "A message"
if (isAjax)
  jsRender("alert(" + jsEscape(msg) + ")")
else
  respondText(msg)

CSRF対策

GET以外のリクエストに対して、Xitrumはデフォルトで Cross-site request forgery 対策を実施します。

antiCsrfMeta Tagsをレイアウト内に記載した場合:

import xitrum.Action
import xitrum.view.DocType

trait AppAction extends Action {
  override def layout = DocType.html5(
    <html>
      <head>
        {antiCsrfMeta}
        {xitrumCss}
        {jsDefaults}
        <title>Welcome to Xitrum</title>
      </head>
      <body>
        {renderedView}
        {jsForView}
      </body>
    </html>
  )
}

出力される <head> は以下のようになります:

<!DOCTYPE html>
<html>
  <head>
    ...
    <meta name="csrf-token" content="5402330e-9916-40d8-a3f4-16b271d583be" />
    ...
  </head>
  ...
</html>

xitrum.js をテンプレート内で使用した場合、 このトークンは X-CSRF-Token ヘッダーとしてGETを除く全てのjQueryによるAjaxリクエストに含まれます。 xitrum.jsは jsDefaults タグを使用することでロードされます。 もし jsDefaults を使用したくない場合、以下のようにテンプレートに記載することですることでxitrum.jsをロードすることができます。

<script type="text/javascript" src={url[xitrum.js]}></script>

CSRF対策インプットとCSRF対策トークン

XitrumはCSRF対策トークンをリクエストヘッダーの X-CSRF-Token から取得します。 もしリクエストヘッダーが存在しない場合、Xitrumはリクエストボディの csrf-token から取得します。 (URLパラメータ内には含まれません。)

前述したメタタグとxitrum.jsを使用せずにformを作成する場合、antiCsrfInput または antiCsrfToken を使用する必要があります。

form(method="post" action={url[AdminAddGroup]})
  != antiCsrfInput
form(method="post" action={url[AdminAddGroup]})
  input(type="hidden" name="csrf-token" value={antiCsrfToken})

CSRFチェックの省略

スマートフォン向けアプリケーションなどでCSRFチェックを省略したい場合、 xitrum.SkipCsrfCheck を継承してActionを作成します。

import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.POST

trait Api extends Action with SkipCsrfCheck

@POST("api/positions")
class LogPositionAPI extends Api {
  def execute() {...}
}

@POST("api/todos")
class CreateTodoAPI extends Api {
  def execute() {...}
}

ルーティングの操作

Xitrumは起動時に自動でルーティングを収集します。 収集されたルーティングにアクセスするには、xitrum.Config.routes を使用します。

例:

import xitrum.{Config, Server}

object Boot {
  def main(args: Array[String]) {
    // サーバーをスタートさせる前にルーティングを操作します。
    val routes = Config.routes

    // クラスを指定してルートを削除する場合
    routes.removeByClass[MyClass]()

    if (demoVersion) {
      // prefixを指定してルートを削除する場合
      routes.removeByPrefix("premium/features")

      // '/'が先頭にある場合も同じ効果が得られます
      routes.removeByPrefix("/premium/features")
    }

    ...

    Server.start()
  }
}

リクエストコンテンツの取得

通常リクエストコンテンツタイプが application/x-www-form-urlencoded 以外の場合、 以下のようにしてリクエストコンテンツを取得することができます。

文字列として取得:

val body = requestContentString

文字列として取得し、JSONへのパース:

val myJValue = requestContentJValue  // => JSON4S (http://json4s.org) JValue
val myMap = xitrum.util.SeriDeseri.fromJValue[Map[String, Int]](myJValue)

より詳細にリクエストを扱う場合、 request.getContent を使用することで ByteBuf としてリクエストを取得することができます。

SwaggerによるAPIドキュメンテーション

Swagger を使用してAPIドキュメントを作成することができます。 @Swagger アノテーションをドキュメント化したいActionに記述します。 Xitrumはアノテーション情報から /xitrum/swagger.json を作成します。 このファイルを Swagger UI で読み込むことでインタラクティブなAPIドキュメンテーションとなります。 XitrumはSwagger UI を内包しており、 /xitrum/swagger-ui というパスにルーティングします。 例: http://localhost:8000/xitrum/swagger-ui.

_images/swagger.png

サンプル を見てみましょう。

import xitrum.{Action, SkipCsrfCheck}
import xitrum.annotation.{GET, Swagger}

@Swagger(
  Swagger.Tags("APIs to create images"),
  Swagger.Description("Dimensions should not be bigger than 2000 x 2000"),
  Swagger.OptStringQuery("text", "Text to render on the image, default: Placeholder"),
  Swagger.Produces("image/png"),
  Swagger.Response(200, "PNG image"),
  Swagger.Response(400, "Width or height is invalid or too big")
)
trait ImageApi extends Action with SkipCsrfCheck {
  lazy val text = paramo("text").getOrElse("Placeholder")
}

@GET("image/:width/:height")
@Swagger(  // <-- Inherits other info from ImageApi
  Swagger.Summary("Generate rectangle image"),
  Swagger.IntPath("width"),
  Swagger.IntPath("height")
)
class RectImageApi extends Api {
  def execute {
    val width  = param[Int]("width")
    val height = param[Int]("height")
    // ...
  }
}

@GET("image/:width")
@Swagger(  // <-- Inherits other info from ImageApi
  Swagger.Summary("Generate square image"),
  Swagger.IntPath("width")
)
class SquareImageApi extends Api {
  def execute {
    val width  = param[Int]("width")
    // ...
  }
}

/xitrum/swagger にアクセスすると SwaggerのためのJSON が生成されます。

Swagger UIはこの情報をもとにインタラクティブなAPIドキュメンテーションを作成します。

ここででてきたSwagger.IntPath、Swagger.OptStringQuery以外にも、BytePath, IntQuery, OptStringFormなど 以下の形式でアノテーションを使用することができます。

  • <Value type><Param type> (必須パラメータ)
  • Opt<Value type><Param type> (オプションパラメータ)

Value type: Byte, Int, Int32, Int64, Long, Number, Float, Double, String, Boolean, Date, DateTime

Param type: Path, Query, Body, Header, Form

詳しくは value typeparam type を参照してください。