岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

Pinto公開に向けて #3 ― Addressableを使ってみた

あらすじ

PintoというソーシャルブックマークサービスRubyで作ろうとしています。URI駆動設計がアツいので、前回URIを設計しました。今回はURI Templatesに基づくルーティング処理をAddressableで実装してみます。

おことわり

Rubyはいじり始めたばかりなので、以下、おかしなコードが含まれるかもしれません。お気づきの方はご指摘いただけると、中の人が喜びます。

まずはrackup用スクリプト

Webサーバ依存なコードは書きたくないので、Rackで実装することに以前決めました

RackのREADMEにある通り、rackupというツールを使うと、特別な設定なしにWebサーバが起動でき、RackベースのWebアプリが実行できます。

というわけで、まずはrackup用のスクリプトを書きます。とりあえず下記のようにしました。

# script/server.ru
$LOAD_PATH << '../lib'
require 'pinto/dispatcher'
run Pinto::Dispatcher.new

下記コマンドで実行。

cd script
rackup -s mongrel -p 80 server.ru

次にディスパッチャ

続いて、rackupから呼ばれるディスパッチャを書きます。まだまだ完成形ではないですが、今は下記のようになっています。

# lib/pinto/dispatcher.rb
require 'addressable/uri'
require 'pinto/encoding'
require 'pinto/uri/extract_processor'
require 'rack'

module Pinto
  class Dispatcher
    def call(env)
      uri_templates = {
        'top' =>
          'http://pinto.jp/',
        'index' =>
          'http://pinto.jp/index{lang}',
        'signup_forms_openid' =>
          'http://pinto.jp/signup_forms/openid{lang}',
        'signup_forms_auth' =>
          'http://pinto.jp/signup_forms/auth{lang}{query}',
        'signup_forms_account' =>
          'http://pinto.jp/signup_forms/account{lang}{query}',
        'users' =>
          'http://pinto.jp/users{lang}',
        'user' =>
          'http://pinto.jp/users/{username}{lang}'
      }

      request = Rack::Request.new(env)
      parsed_uri = Addressable::URI.parse(request.url)

      uri_templates.each do |controller, uri_template|
        param = parsed_uri.extract_mapping(uri_template,
                                           Pinto::URI::ExtractProcessor)
        next if param.nil?

        param['query'] = Pinto::Encoding.to_utf8(request.GET)
        param['form'] = Pinto::Encoding.to_utf8(request.POST)

        # require "pinto/controller/#{controller}"
        # ToDo: コントローラ呼び出し処理を実装する

        return [
          200,
          {'Content-Type' => 'text/plain; charset=UTF-8'},
          ['controller: ' + controller + "\nparam: " + param.inspect]
        ]
      end

      [
        404,
        {'Content-Type' => 'text/plain; charset=UTF-8'},
        ['Not Found']
      ]
    end
  end
end

uri_templatesハッシュに、コントローラ名とURIテンプレートを定義しています。各ブラケット(URI Templatesでは「template expansion」と呼ぶらしい)の意味は下記の通りです。

{lang}
「.ja」「.en」または空文字列。言語指定用の拡張子のようなイメージです。
{query}
クエリストリング(空の場合もある)。URI Templatesのjoinオペレータで定義しても良さそうですが、現状のAddressableはjoinに対応していないので、ざっくりとにしました。
{username}
ユーザ名。文字種は決めていませんが、とりあえず英数字といくつかの記号でしょう。

続いてextract用プロセッサ

Addressable::URI#extract_mappingURIテンプレートに実際のURLをぶつけてブラケット部分の値を抜き出す(extract)のですが、このとき第2引数としてプロセッサが渡せます。

プロセッサの実装については、リンク先のサンプルコードを見ていただくのが早いのですが、簡単にいうと、self.matchではマッチング用の正規表現が、self.restoreでは実際に取得したい値が指定できます。

先に定義したテンプレートに基づき、下記のように書きました。

# lib/pinto/uri/extract_processor.rb
module Pinto
  class URI
    class ExtractProcessor
      def self.match(name)
        return '\.ja|\.en|'      if name == 'lang'
        return '\?.+|'           if name == 'query'
        return '[0-9a-zA-Z_\-]+' if name == 'username'
        return '.*'
      end

      def self.restore(name, value)
        return value.gsub(/^\./, '') if name == 'lang'
        return value.gsub(/^\?/, '') if name == 'query'
        return value
      end
    end
  end
end

matchではもっとスマートな正規表現が書きたいのですが、例えばカッコが含まれたりするとうまく動きませんでした。追究するならAddressableのコードを読まないとダメですね。

おまけ:文字コード変換スクリプト

ディスパッチャの中で Pinto::Encoding.to_utf8(request.GET) などとしているわけですが、これ、そもそも必要なのかどうか、よく分かっていません(えー)。そうしないと日本語が正しく表示できなかったので、今のところそうしているだけです。

せっかくなので、コードを晒します。

# lib/pinto/encoding.rb
require 'kconv'

module Pinto
  class Encoding
    def self.to_utf8(param)
      return param.toutf8 if param.respond_to? 'toutf8'

      if param.kind_of? Array
        return param.map do |v|
          self.to_utf8(v)
        end
      end

      if param.kind_of? Hash
        return param.map do |k, v|
          [self.to_utf8(k), self.to_utf8(v)]
        end
      end

      return param
    end
  end
end

Ruby 1.9ではM17N対応が行われ、この辺りの処理がだいぶ変わるはずなので、今は気にしないでおこうと思っています。

次のアクション

  • コントローラの呼び出し部分を実装する