DRYな備忘録

Don't Repeat Yourself.

(訳しながら)つくって覚えるRevelフレームワーク - その2

前回にひきつづき、Golangどころかフレームワークのなんたるかを理解していない僕はRevelのドキュメントを和訳しつつ、理解を深めたいでござる。

それでは

今回はOverviewからでござる。

f:id:otiai10:20140107194128j:plain

Overview

This section gives you a taste of various parts of the framework:

このセクションでは、Revelフレームワークの構成を軽く紹介していきます。

  • Routing - A simple declarative routing syntax. Type-safe reverse routing.

  • Controllers - Revel organizes endpoints into Controllers. They provide easy data binding and form validation.

    • Revelはエンドポイントとコントローラを対応させます。コントローラーは簡単なデータの取得やフォーム値のバリデーションを行います。
  • Templates - Revel makes Go Templates simple to use at scale.

    • テンプレートは、Go Templateをよりスケールしやすくシンプルにしたものです。
  • Interceptors - Register functionality to be called before or after actions. They can be activated per Controller.

    • インターセプタは、actionに対するいわゆるbeforeやafterの機能を提供します。コントローラーごとに設定が可能です。
  • Filters - More general functionality can be implemented with Filters.

    • フィルターでは、より包括的な機能を独自実装することができます。

Routing

Revel uses a declarative routing syntax. It collects all routes for an app in a single file, with a simple syntax for matching requests, extracting arguments from URIs, and specifying route-specific arguments to the action. Here's a commented sample...

Revelは宣言型のルーティング表記を採用しています。一つのファイルにすべてのルート定義を集め、シンプルな文字列一致で表記され、URIに含まれるパラメータを抽出し、各ルートで定義されている変数に格納します。以下のコメント付きサンプルを見てください、

# conf/routes
# すべてのルーティング定義をこのファイルに表記 (マッチング優先順)
GET    /login                Application.Login       # シンプルなルート
GET    /hotels/              Hotels.Index            # 最後のスラッシュが無くてもマッチ
GET    /hotels/:id           Hotels.Show             # hotels/以下を変数として取得
WS     /hotels/:id/feed      Hotels.Feed             # ウェブソケットを提供(Σ(゚◇゚;)マジデッ!?
POST   /hotels/:id/:action   Hotels.:action          # 動的なアクション定義
GET    /public/*filepath     Static.Serve("public")  # 静的リソースへのパス
*      /:controller/:action  :controller.:action     # すべてにマッチ、完全な動的ルーティング

リバースルーティングもタイプセーフな形で提供されます。以下に例を示します、

// hotelの情報をidによって表示
func (c Hotels) Show(id int) revel.Result {
    hotel := HotelById(id)
    return c.Render(hotel)
}

// hotel情報の更新を保存し、上記のShowにリダイレクトする
func (c Hotels) Save(hotel Hotel) revel.Result {
    // (ここでバリデーションとか永続化とか)
    return c.Redirect(routes.Hotels.Show(hotel.Id))
}

Controllers

(´-`).。oO( このあたりから原文載せるのめんどくさくなってきた...

すべてのアクションはコントローラーのメソッドです。この特徴はいくつかのイケてる機能を提供します:

  • Data binding
    • RevelはURLやフォーム値を値や構造体にして、各メソッド(アクション)に受け渡します。(パラメータのマッピングから直接取るのももちろんおkです)
  • Validation
    • バリデーションやそれに伴うエラーを扱うヘルパーがあります。
  • Flash
    • 1リクエストのみで有効なクッキーです(エラーや成功メッセージなどに使えますね)。
  • Session
    • 暗号でひもづけられたクッキーです。map[string](type string)の形で取得できます。
  • Results
    • リバースルーティングを用いたリダイレクトや、ローカルスコープの変数を用いたテンプレートレンダリングができます。

以下サンプル

// app/controllers/app.go

type Application struct {
    *revel.Controller
}

func (c Application) Register() revel.Result {
    title := "Register"
    return c.Render(title)
}

func (c Application) SaveUser(user models.User, verifyPassword string) revel.Result {
    c.Validation.Required(verifyPassword)
    c.Validation.Required(verifyPassword == user.Password)
        Message("Password does not match")
    user.Validate(c.Validation)

    if c.Validation.HasErrors() {
        c.Validation.Keep()
        c.FlashParams()
        return c.Redirect(routes.Application.Register())
    }

    user.HashedPassword, _ = bcrypt.GenerateFromPassword(
        []byte(user.Password), bcrypt.DefaultCost)
    err := c.Txn.Insert(&user)
    if err != nil {
        panic(err)
    }

    c.Session["user"] = user.Username
    c.Flash.Success("Welcome, " + user.Name)
    return c.Redirect(routes.Hotels.Index())
}

Templates

RevelはGoTemplateをウェブアプリ用に統合していると考えて結構です。以下のコードは、前述のRegister actionに対応するテンプレートがレンダーされる例です。

特徴

  • Revelはactionで使われいる変数名をtemplateの中から見つけ出します
  • fieldはバリデーションのエラーやパラメータの値に名前をつけてマップするヘルパーです。ヘルパー関数はコードの中でいつでも使用可能です。
  • title変数はやや特殊で、あたかもレンダーパラメータで渡されたもののように参照できます。(例ではheader.htmlの中で使われています)
{{/* app/views/Application/Register.html */}}
{{/* ちなみにここはコメント */}}

{{template "header.html" .}}

<h1>Register:</h1>
<form action="/register" method="POST">
  {{with $field := field "user.Username" .}}
    <p class="{{$field.ErrorClass}}">
      <strong>Username:</strong>
      <input type="text" name="{{$field.Name}}" size="16" value="{{$field.Flash}}"> *
      <span class="error">{{$field.Error}}</span>
    </p>
  {{end}}

  {{/* other fields */}}

  <p class="buttons">
    <input type="submit" value="Register"> <a href="/">Cancel</a>
  </p>
</form>

{{template "footer.html" .}}

Interceptors

インターセプタは、リクエストの前後処理(もしくはレスポンスの失敗処理)を定義するコントローラのメソッドです。特定のコントローラを他のコントローラに持たせることで、共通のインターセプタを複数のコントローラで共有することができます。

たとえば、データベースモジュールを使用するときは初期化で接続を確保することになると思いますが、これを共通のハンドラで定義・利用できるようになります。また、db.Transactional型を持たせることで、sql.Txnフィールドが使用できるようになり、加えて、トランザクションを開始しコミット(および失敗時のロールバック)できるインターセプタも使用できるようになります。

インターセプタの振る舞いは以下の例(負の値のエラーハンドリング)を見れば分かるかと思います。

// github.com/robfig/revel/modules/db/app/db.go

var Db *sql.DB

func Init() {
    // 設定を読み...
    Driver, _ = revel.Config.String("db.driver")
    Spec, _ = revel.Config.String("db.spec")

    // 接続の開始する
    Db, _ = sql.Open(Driver, Spec)
}

// `Transactional`はコントローラにトランザクション管理を提供します
type Transactional struct {
    *revel.Controller
    Txn *sql.Tx
}

func (c *Transactional) Begin() revel.Result {
    c.Txn, _ = Db.Begin()
    return nil
}

func (c *Transactional) Commit() revel.Result {
    _ = c.Txn.Commit()
    c.Txn = nil
    return nil
}

func (c *Transactional) Rollback() revel.Result {
    _ = c.Txn.Rollback()
    c.Txn = nil
    return nil
}

// 前処理、後処理、失敗処理にそれぞれ、
// `Transactional`型の
// Begin, Commit, Rollbackを割り当て(Intercept)ている
func init() {
    revel.InterceptMethod((*Transactional).Begin, revel.BEFORE)
    revel.InterceptMethod((*Transactional).Commit, revel.AFTER)
    revel.InterceptMethod((*Transactional).Rollback, revel.PANIC)
}

で、このインターセプタがアプリケーションのコントローラの中でいかに動くかというと、

// Bookings structはrevel.Controllerであり、
// db.Transactionalであり
// user.Loginも持っている
// (これがダックタイピングというやつか?)
type Bookings struct {
    *revel.Controller
    db.Transactional  // Adds .Txn
    user.Login        // Adds .User
}

// BookingsコントローラーのShowFirstBookingメソッド...
// の前にTransactional.Beginが呼ばれ、
// の中で失敗すればTransaction.Rollbackが呼ばれ、
// の後にTransactional.Commitが呼ばれている
func (c Bookings) ShowFirstBooking() revel.Result {
    row := c.Txn.QueryRow(`
select id, hotel_id, user_id, price, nights
  from Booking
 where UserId = ?
 limit 1`, c.User.Id)
    ...
    return c.Render(booking)
}

Filters

Filtersは、アプリケーションのミドルウェアです。これらは特定の名前を持った関数に他なりません。

type Filter func(c *Controller, filterChain []Filter)

interceptorなどの複雑なビルドイン機能も、filterとして実装されています。

// github.com/robfig/revel/intercept.go

var InterceptorFilter = func(c *Controller, fc []Filter) {
    defer invokeInterceptors(FINALLY, c)
    defer func() {
        if err := recover(); err != nil {
            invokeInterceptors(PANIC, c)
            panic(err)
        }
    }()

    // Beforeインターセプタを呼び出し、resultを得次第、returnする
    invokeInterceptors(BEFORE, c)
    if c.Result != nil {
        return
    }

    fc[0](c, fc[1:])
    invokeInterceptors(AFTER, c)
}

Revelがデフォルトで備えているFiltersのセットはいずれも、開発者によってオーバーライドすることが可能です。Filtersがあることによって、フレームワークの意図した特定の部分だけ意図通りカスタマイズすることが可能です。

// github.com/robfig/revel/filter.go

// Filters is the default set of global filters.
// It may be set by the application on initialization.
var Filters = []Filter{
    PanicFilter,             // Recover from panics and display an error page instead.
    RouterFilter,            // Use the routing table to select the right Action
    FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
    ParamsFilter,            // Parse parameters into Controller.Params.
    SessionFilter,           // Restore and write the session cookie.
    FlashFilter,             // Restore and write the flash cookie.
    ValidationFilter,        // Restore kept validation errors and save new ones from cookie.
    I18nFilter,              // Resolve the requested language
    InterceptorFilter,       // Run interceptors around the action.
    ActionInvoker,           // Invoke the action.
}

ほぼすべてのフレームワーク機能はこのFiltersとして実装されています。また、これらはすべてフレームワークの設定として開発者にパブリックな状態で存在しています。Revelフレームワークが理解しやすく簡潔なモジュール構成なのは、このためです。

その証拠に、主要なサーバ機能が如何にシンプルかお見せしましょう

// github.com/robfig/revel/server.go

func handleInternal(w http.ResponseWriter, r *http.Request, ws *websocket.Conn) {
    var (
        req  = NewRequest(r)
        resp = NewResponse(w)
        c    = NewController(req, resp)
    )
    req.Websocket = ws

    Filters[0](c, Filters[1:])
    if c.Result != nil {
        c.Result.Apply(req, resp)
    }
}

とりあえず

今回はここまで。次回からはいよいよ、Introductionをやって、アプリケーションをつくっていきたいでござる。

しかし、

英語の勉強にもなるし、すごくソフトウェアの勉強になるますまる