Lift の RestHelper でサクサクAPI 開発

 

 

この記事はScala Advent Calendar 2013の2日目の記事です。前日はshoma2daさんの2014年こそScalaを始めようでした。

さて、今年 Scala でとあるWebサービスを開発しまして、そこで利用した Lift の RestHelper の話でも。

とあるWebサービスについて

本題に入る前にちょろっと宣伝。今年の3月に RankPlat というAndroidアプリ開発者向けのランキングプラットフォームをリリースしました。いわゆるカジュアルゲームでのユーザランキング機能を提供してくれるサービスですね。無料で使える上にランキング画面(Web)に開発者が用意した広告を表示できたりします。僕個人は既にこのプロジェクトから抜けているため詳細は不明ですが、今後 iOS に対応する予定もあるとかないとか…。

RankPlat は Android に提供している SDK を除いて、全て Scala の Lift で作っています。なぜ Lift を選択したかというと、Android アプリとデータをやりとりする API を開発するにあたって Lift の RestHelper が便利そうだったからですね。

Lift について

Lift についても軽く触れておきます。Lift は Scala で書かれた Webフレームワークですね。

Play が出る以前は Scala で Webフレームワークといえば Lift だったようですが、Play2 がデフォルトで Scala をサポートしてから Lift の話はめっきり聞かなくなりました。。Snippet アプローチによる View を中心としたアーキテクチャが一般受けしてない気がします…。僕はこのアーキテクチャ好きなんですけどね。。

RestHelper について

ようやく本題。Lift には REST な Web サービスをシンプルに作成するための RestHelper という trait が用意されてます。使い方についてはこちらにもまとまってますが、ざっくり説明すると

  1. RestHelper を extends した object を作る。
    object BootRest extends RestHelper {  
    }
    
  2. bootstrap.liftweb で  BootRest を使う LiftRule を追加する。
    LiftRules.dispatch.append(BootRest)  
    LiftRules.statelessDispatchTable.append(BootRest)  
    
  3. BootRest にAPI の実装をしていく。
    object BootRest extends RestHelper {  
      serve {
        case Req("api" :: "static" :: _, "xml", GetRequest) => <b>Static</b>
        case Req("api" :: "static" :: _, "json", GetRequest) => JString("Static")
      }
    }
    

といった感じになります。

まぁこれだけでも十分なんですが、これだと API が増えるたびに BootRest に case 文を追加しなければならないため、リフレクションと trait を利用したポリモーフィズムでサクサク開発できるようにしていきます。

ApiTrait を作成

ポリモーフィズムしたいので API 用の trait を作ります。

package me.a4p.api.core

import scala.xml._  
import net.liftweb.json.DefaultFormats  
import net.liftweb.json.Extraction  
import net.liftweb.json.DefaultFormats  
import net.liftweb.json.JsonAST.JValue  
import net.liftweb.json.Xml  
import net.liftweb.common.Box

trait ApiTrait {  
  // xml 形式でデータを返す
  final def toXml: Node = {
    Xml.toXml(toJson).apply(0)
  }

  // json 形式でデータを返す
  final def toJson: JValue = {
    implicit val formats = DefaultFormats
    Extraction.decompose(run)
  }

  // パラメータ取得処理。リクエストから取得したパラメータをMapに格納して返す。
  def getParam : Map[String, Box[String]]
  // 入力値チェック。エラーを Map の List で返却。
  def validate(params : Map[String, Box[String]]) : List[Map[String, String]]
  // メイン処理。パラメータを元に返却値をMapで返す。
  def execute(params : Map[String, Box[String]]) : Map[String, Any]

  // getParam, validate, execute をまとめたもの。
  def run : Map[String, Any] = {
    val params = getParam
    val errors = validate(params)
    if (!errors.isEmpty) {
      Map("resultSet" -> Map("statusCode" -> 400, "errors" -> errors))
    } else {
      try {
        Map("resultSet" -&gt; Map("statusCode" -> 200, "result" -> execute(params)))
      } catch {
        case e : Exception => e.printStackTrace; Map("resultSet" -> Map("statusCode" -> 500))
      }
    }
  }
}

RestHelper 実装クラスで ApiTrait の実装クラスをリフレクションで呼び出す

次に RestHelper の実装クラスで ApiTrait の実装クラスをリフレクションで呼び出すようにします。

package me.a4p.api.core

import net.liftweb.http._  
import net.liftweb.http.rest._  
import net.liftweb.json.JsonAST.{JString,JValue}  
import scala.xml.Elem  
import net.liftweb.common.Full  
import me.a4p.api.error._

object BootRest extends RestHelper {  
  // api の実装クラスを配置するデフォルトパッケージ
  val apiPackage = "me.a4p.api."
  serve {
    case Req(ver :: appName :: _, "xml", PostRequest) =>
      Full(
        try {
          val appInstance = this.getClass.getClassLoader.loadClass(apiPackage + ver + "." + appName).newInstance
          appInstance match {
            case app:ApiTrait => app.toXml
            case _ => Error501.toXml
          }
        } catch {
          case e:ClassNotFoundException => Error404.toXml
          case e:Exception => Error500.toXml
        }
      )
    case Req(ver :: appName :: _, "json", PostRequest) =>
      Full(
        try {
          val appInstance = this.getClass.getClassLoader.loadClass(apiPackage + ver + "." + appName).newInstance
          appInstance match {
            case app:ApiTrait => app.toJson
            case _ => Error501.toJson
          }
        } catch {
          case e:ClassNotFoundException => Error404.toJson
          case e:Exception => Error500.toJson
        }
      )
    // Getは受け付けない
    case Req(ver :: appName :: _, "xml", GetRequest) => Full(Error403.toXml)
    case Req(ver :: appName :: _, "json", GetRequest) => Full(Error403.toJson)
  }
}

※ Error404 などは ApiTrait を継承して作成したエラー値を返却するオブジェクトです。

これで、api の実装クラスを配置するデフォルトパッケージ配下に ApiTrait を継承したクラスを配置すれば URL でクラス名をリフレクションして呼び出せるようになりました。

上のソースコードの場合、me.a4p.api.ver1.SaveData に対して、http://hoge.com/ver1/SaveData/call.json または http://hoge.com/ver1/SaveData/call.xml でアクセスできるようになります。

API の実装

というわけで API の実装サンプルです。

package me.a4p.api.ver1

import net.liftweb.http.S  
import net.liftweb.common.{Full,Box, Empty}  
import me.a4p.api.core.ApiTrait

class SaveData extends ApiTrait {

  override def getParam() : Map[String, Box[String]] = {
    // リクエストからパラメータを取得して Map に格納する
    Map("userId" -> S.param("userId"), "score" -> S.param("score"))
  }

  override def validate(params : Map[String, Box[String]]) : List[Map[String, String]] = {
    // 入力パラメータのチェック。
    if (params.isEmpty) {
      List(Map("code" -> "1", "message" -> "パラメータがねえです"))
    } else {
      List.empty
    }
  }

  override def execute(params : Map[String, Box[String]]) = {
    // データを保存する処理
    save(params("userId"), params("score"))
    Map.empty
  }
}

こんな感じで、パラメータ取得・入力チェック・実処理を書くだけで json/xml のAPIが作成できます。

 

さいごに

Lift は RestHelper 以外にも Web フレームワークとしてかなり高機能なのでみんなもっと使ったらいいと思います。何か困っても Stack Overflowソースコードを見れば大抵解決できます。マジおすすめ。