Scalatra2.2+scalatra-swaggerでREST APIのリファレンスを生成する #2

2013.02.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

REST APIの作成

前回の続きです。まずは、適当なREST APIを実装します。

SwaggerTestController.scala

package jp.classmethod.scalatraswagger

import org.scalatra._
import org.scalatra.json._
import org.json4s._

class SwaggerTestController extends ScalatraServlet with JacksonJsonSupport {

  protected implicit val jsonFormats: Formats = DefaultFormats
  private[this] val postRepo = new PostRepository

  before() {
    contentType = formats("json")
  }

  get("/") {
    params.getAs[String]("name") match {
      case Some(name) => postRepo getPostsByName name
      case None => postRepo.allPosts 
    }
  }

  post("/") {
    parsedBody.extractOpt[Post] match {
      case Some(post) => 
        postRepo addPost post
        Ok()
      case _ => halt(400, "invalid params")
    }
  }

  notFound {
    halt(404)
  }
}

Post.scala

package jp.classmethod.scalatraswagger

case class Post(name: String, comment: String)

class PostRepository {

  private[this] var posts: IndexedSeq[Post] = IndexedSeq.empty

  def addPost(post: Post) {
    posts +:= post
  }

  def allPosts = posts

  def getPostsByName(name: String) = {
    posts filter { _.name == name }
  }
}

名前とコメントからなる投稿の、一覧の取得と追加機能をAPIとして提供しています。GETとPOSTだけの簡単なサンプルです。

コントローラを作成したら、Scalatraのブートストラップに登録します。

Scalatra.scala

import jp.classmethod.scalatraswagger._
import org.scalatra._
import javax.servlet.ServletContext

/**
 * This is the Scalatra bootstrap file. You can use it to mount servlets or
 * filters. It's also a good place to put initialization code which needs to
 * run at application start (e.g. database configurations), and init params.
 */
class Scalatra extends LifeCycle {
  override def init(context: ServletContext) {
    // REST APIを叩くサンプルクライアントページを返すコントローラをマウント
    context.mount(new RootController, "/*")
    // REST APIのルーティングを行うコントローラをマウント
    context.mount(new SwaggerTestController, "/posts/*")
  }
}

これでサンプルREST APIは完成です。適当に作ったクライアントから叩いてきちんと動作するか確認しておきます。以下のスクリーンショットは、クライアントからAPIを叩いて数回POSTした結果をGETした際の様子です。

scalatra-swagger01

Swagger specを提供するコントローラを作成する

コントローラを作成

では、準備が整ったので、APIのコントローラをSwaggerに対応させます。まずは、Swagger specをクライアントに提供するコントローラを作成します。

ResourcesApp.scala

package jp.classmethod.scalatraswagger

import org.scalatra.swagger.{JacksonSwaggerBase, Swagger, SwaggerBase}
import org.scalatra.ScalatraServlet
import com.wordnik.swagger.core.SwaggerSpec

class ResourcesApp(implicit val swagger: Swagger) extends ScalatraServlet with JacksonSwaggerBase

class SwaggerTestSwagger extends Swagger(SwaggerSpec.version, "1.0")

このコントローラは通常のコントローラ同様、ScalatraServlet抽象クラスを実装したサーブレットとして作成します。このサーブレットにJacksonSwaggerBaseトレイトをミックスインすればSwagger specを提供するコントローラの出来上がりです。

また、コンストラクタではSwagger型のオブジェクトを暗黙のパラメータとして取るよう実装されています。これは、JacksonSwaggerBaseのスーパートレイトであるSwaggerBaseトレイトで、Swagger型の抽象メソッドswaggerが定義されているためです。Swagger型のオブジェクトは、APIリファレンスに記述される情報を管理するオブジェクトです。JacksonSwaggerBaseトレイトではこのメソッドが実装されていないため、ResourceAppでswaggerを実装しています。

9行目では、このResourceAppに渡すSwagger型のクラスとして、Swaggerクラスのサブクラスを実装しています。コンストラクタで渡しているのは、Swagger specのバージョンとREST APIのバージョンを表すStringです。

コントローラをブートストラップに登録

先ほど作成したコントローラをScalatraのブートストラップに登録します。

Scalatra.scala

import jp.classmethod.scalatraswagger._
import org.scalatra._
import javax.servlet.ServletContext

/**
 * This is the Scalatra bootstrap file. You can use it to mount servlets or
 * filters. It's also a good place to put initialization code which needs to
 * run at application start (e.g. database configurations), and init params.
 */
class Scalatra extends LifeCycle {

  implicit val swagger = new SwaggerTestSwagger

  override def init(context: ServletContext) {
    // REST APIを叩くサンプルクライアントページを提供するコントローラをマウント
    context.mount(new RootController, "/*")
    // REST APIのルーティングを行うコントローラをマウント
    context.mount(new SwaggerTestController, "/posts/*")
    // SwaggerのJSON specを提供するコントローラをマウント
    context.mount(new ResourcesApp, "/api-docs/*")
  }
}

20行目でResourceAppを"api-docs"というurlでアクセスできるように設定しています。また12行目では、ResourceAppのコンストラクタに暗黙のパラメータとして渡すSwaggerTestSwagger型のインスタンスを生成しています。

Swagger specを取得してみる

では、ブラウザでhttp://localhost:8080/api-docs/resources.jsonにアクセスしてみます。すると、以下のようなJSON形式のSwagger specが返されます。

{"basePath":"http://localhost:8080","swaggerVersion":"1.1","apiVersion":"1.0","apis":[]}

今の段階ではまだリソースの情報が記述されていません。

リファレンスの内容をREST APIのコントローラに記述する

Swagger specが取得できるようになりましたので、リソースの各操作の情報をREST APIのコントローラに記述していきます。

SwaggerSupportトレイトをミックスイン

コントローラにSwagger specの内容を記述するために、SwaggerSupportトレイトをミックスインします。

SwaggerTestController.scala

import org.scalatra.swagger._
...
class SwaggerTestController(implicit val swagger: Swagger) extends ScalatraServlet with JacksonJsonSupport with SwaggerSupport {
  ...
}

このコントローラも、Scalatraのブートストラップで暗黙のパラメータとして宣言されたSwaggerTestSwaggerのインスタンスをコンストラクタで受け取ります。

APIの概要を記述

コントローラに、APIの概要としてAPIの名前とその説明を記述します。

SwaggerTestController.scala

class SwaggerTestController(implicit val swagger: Swagger) extends ScalatraServlet with JacksonJsonSupport with SwaggerSupport {
  ...
  // Swagger specに出力するAPIの名前
  override protected val applicationName = Some("posts")
  // Swagger specに出力するAPIの概要
  protected val applicationDescription = "コメントの投稿と、投稿されたコメントの取得機能を提供するAPIです。"
  ...
}

applicationNameにAPIの名前を、applicationDescriptionにAPIの概要を記述します。この2つの変数はSwaggerSupportトレイトのスーパートレイトであるSwaggerSupportSyntaxトレイトでメソッドとして定義されているものを、それぞれオーバーライド・実装しています。

アクションに記述

最後に、コントローラの各アクションに提供するAPIの情報を定義します。

SwaggerTestController.scala

get("/", operation(
  apiOperation[List[Post]]("getPosts")
  summary "全ての投稿を取得"
  notes "全ての投稿を取得します。名前による絞り込みもできます。"
  parameter queryParam[String]("name").description("取得する投稿の名前").optional
  )) {
  params.getAs[String]("name") match {
    case Some(name) => postRepo getPostsByName name
    case None => postRepo.allPosts 
  }
}

post("/", operation(
  apiOperation[Unit]("addPost")
  summary "コメントを投稿"
  notes """|コメントを投稿します。必ず名前(name)とコメント(comment)をJSON形式のパラメータとして渡す必要があります。
       |<br>どちらかが渡されなかった場合、400を返します。""".stripMargin
  parameter bodyParam[Post]("post").description("投稿データ").required
  error Error(400, "パラメータが不正")
  )) {
  parsedBody.extractOpt[Post] match {
    case Some(post) => 
      postRepo addPost post
      Ok()
    case _ => halt(400, "invalid params")
  }
}

Scalatraのgetメソッドの最初の引数リストは元々RouteTransformer型の可変長引数になっています。scalatra-swaggerではこれを利用してリファレンスに記述する情報のメタデータを定義しています。

OperationBuilder

apiOperationメソッドはOperationBuilder型を返すメソッドです。型パラメータで戻り値の型を、引数でnicknameを指定します。nicknameは、Swaggerがメソッドを識別するために利用するキーで、主にクライアントでのメソッド生成に利用されています。

OperationBuilderは各オペレーションの情報を設定するためのビルダで、このOperationBuilderの各種メソッドを利用して情報を記述していきます。OperationBuilderのメソッドは戻り値として自身を返すので、メソッドチェーンを組むことができます。上記コードの様に、メソッドチェーンを中置記法で書くと内容が見やすくなります。

OperationBuilderのメソッドは、SwaggerOperationBuilderトレイトで実装されており、以下のメソッドが利用できます。

summary
対象のオペレーションで行う処理の要約を簡潔に記述します。

notes
対象のオペレーションで行う処理の要約を記述します。
parameter/parameters
このオペレーションで受け取ることのできるパラメータを指定します。パラメータ1つにつき、Parameter型のインスタンス1つを引数に渡します。上記コードではクエリストリングでパラメータを渡すので、queryParamメソッドでParameter型のインスタンスを生成し、型パラメータとしてクエリストリングのパラメータの型であるString型を渡しています。さらに、descriptionメソッドでパラメータの説明を設定し、optionalメソッドで任意のパラメータであるとをマークしています。
deprecated
オペレーションを非推奨としてマークします。
error/errors
返されるエラーコードとその理由を記述します。org.scalatra.swagger.Error型のケースクラスを引数に渡します。

以上で、各オペレーションの情報の記述は完了です。これで、Swagger specが生成されるようになりました。

Swagger specを取得してみる

では、再度ブラウザでhttp://localhost:8080/api-docs/resources.jsonにアクセスしてみます。すると、以下のようなJSON形式のSwagger specが返されます。

{"basePath":"http://localhost:8080","swaggerVersion":"1.1","apiVersion":"1.0","apis":[{"path":"/api-docs/posts.{format}","description":"コメントの投稿と、投稿されたコメントの取得機能を提供するAPIです。"}]}

apisキーの内容が記述されており、postsリソースの詳細情報を取得するパスなどが定義されています。

生成されたリファレンスを確認する

では、生成されたSwagger specをクライアントで利用してリファレンスを表示させてみましょう。クライアントは、swagger-uiのGitHubから手に入れてもいいですが、Swaggerのデモサイトを利用するのが一番手軽です。デモサイトを開いたら、サイト上部の中央にあるURL入力欄に、http://localhost:8080/api-docs/resources.jsonと入力してEnterキーを押下します。

scalatra-swagger02

すると、Swagger specがロードされて以下のようなページが表示されます。

scalatra-swagger03a

ビューを展開して詳細情報を表示しすると下のような感じです。

scalatra-swagger03b scalatra-swagger03c

なお、Swaggerのデモサイトのクライアントがlocalhostにアクセスできているのは、ScalatraのCorsSupportトレイトがCross-Origin Resource Sharingをデフォルトで全てのドメインに対して許可するよう設定しているためです。CorsSupportトレイトは、SwaggerTestControllerでミックスインしているSwaggerSupportトレイトによってミックスインされています。この動作を変更する場合はCorsSupportトレイトの動作を修正する必要があります。

まとめ

scalatra-swaggerのおかげで、とても簡単にSwagger specを生成することができました。また、各アクションに定義したオペレーションの情報は、ソースコードのコメントの代わりにもなるので、常にソースコードとドキュメントの内容を一致させることがとても楽にできそうです。最初にSwaggerの使い方を覚える必要がありますが、プロジェクトによってはそのコストに見合う効果が得られそうだと思いました。

ソースコードはGitHubに公開しています。