GrowthForecastや社内のサーバアラートビュアーで使っているWeb Application Framework Kossyの使い方。

KossyはCPANにリリースされているので、cpanm でインストールができます

$ cpanm Kossy

アプリケーションのひな形を作る kossy-setup というコマンドもインストールされるので、これを使います。今回のサンプルアプリケーションの名前は KoPaste としましょう。

$ kossy-setup KoPaste
mkdir lib/KoPaste
mkdir views
mkdir public
mkdir public/js
mkdir public/css
mkdir public/images
mkdir t
writing lib/KoPaste.pm
writing t/00_compile.t
writing lib/KoPaste/Web.pm
writing app.psgi
writing views/index.tx
writing views/base.tx
writing Makefile.PL
writing public/favicon.ico
writing public/js/bootstrap-alerts.js
writing public/css/bootstrap.min.css
writing public/js/jquery-1.7.1.min.js
writing public/js/bootstrap-modal.js

スケルトンのコードとcss/jsなどが展開されました。早速ディレクトリに移動し、plackupでサーバを起動してみます。

$ cd KoPaste
$ plackup -r app.psgi

すごく… bootstrapです.. ///

kossy-setup.png

ここからNoPasteに必要な機能をつくっていきましょう。今回作るアプリケーションは十分に小さいのでデータを操作する関数も一つのクラスにいれます。

KoPaste::Web が Dispatcherクラスなのでこれを編集します

use DBIx::Sunny;
use Digest::SHA;

sub dbh {
    my $self = shift;
    my $db = $self->root_dir .'/nopaste.db';
    $self->{_dbh} ||= DBIx::Sunny->connect("dbi:SQLite:dbname=$db",'','',{
        Callbacks => {
            connected => sub {
                my $conn = shift;
                $conn->do(<<EOF);
CREATE TABLE IF NOT EXISTS entry (
    id VARCHAR(255) NOT NULL PRIMARY KEY,
    nickname VARCHAR(255) NOT NULL,
    body TEXT,
    created_at DATETIME NOT NULL
);
EOF
                $conn->do(q{CREATE INDEX IF NOT EXISTS index_created_at ON entry ( created_at )});
                return;
            },
        },
    });
}

データベースとしてSQLiteを使って、Callbackで接続時にテーブルがなければ作ります。 $self->root_dir はapp.psgiが配置されているディレクトリが入っています。

がりっとエントリー操作用のメソッドも追加します

sub add_entry {
    my $self = shift;
    my (  $body, $nickname ) = @_;
    $body = '' if ! defined $body;
    $nickname = 'anonymous' if ! defined $nickname;
    my $id = substr Digest::SHA::sha1_hex($$ . join("\0", @_) . rand(1000) ), 0, 16;
    $self->dbh->query(
        q{INSERT INTO entry (id,nickname,body,created_at) VALUES ( ?, ?, ?, DATETIME('now') )},
        $id, $nickname, $body
    );
    return $id;
}

sub entry_list {
    my $self = shift;
    my $offset = shift;
    $offset = 0 if defined $offset;
    my $rows = $self->dbh->select_all(
        q{SELECT * FROM entry ORDER BY created_at DESC LIMIT ?,11},
        $offset
    );
    my $next;
    $next = pop @$rows if @$rows > 10;
    return $rows, $next;
}

dbh->query や dbh->select_all は DBIx::Sunnyが提供するショートカットメソッドです。

ここから Dispatcherの実装です。「/」にアクセスした際に実行されるコードは、スケルトンでは

get '/' => [qw/set_title/] => sub {
    my ( $self, $c )  = @_;
    $c->render('index.tx', { greeting => "Hello" });
};

このように書かれています。これをエントリーのリストが表示されるようにしましょう

get '/' => sub {
    my ( $self, $c )  = @_;
    my $result = $c->req->validator([
        'offset' => {
            default => 0,
            rule => [
                ['UINT','ivalid offset value'],
            ],
        },
    ]);
    $c->halt(403) if $result->has_error;
    my ($entries,$has_next) = $self->entry_list($result->valid('offset'));
    $c->render('index.tx', {
        offset => $result->valid('offset'),
        entries => $entries,
        has_next => $has_next,
    });
};

Dispatcherのメソッドには、2つの引数が渡されます。1つ目が KoPaste::Webのオブジェクト。2つ目がKossy::Connectionオブジェクトです。 KoPaste::Webオブジェクトはアプリケーション初期化時点からプロセスが死ぬまで有効なオブジェクトで、もちろんKoPaste::Webの他のメソッドの呼び出しに利用できます。Kossy::Connectionオブジェクトはリクエスト単位で生成されるオブジェクトとなります。

ここででてきたvalidatorはKossy内蔵のKossy::Validatorです。ページングする際のoffestについてバリデーションを定義しています。正の整数だけを受け取り、もしパラメータがないときには、0がデフォルトしています。

もしoffsetの値が正しくなければ、403 Forbiddenを返します。

$c->halt(403) if $result->has_error;

そして、上で定義したentry_listメソッドを叩き、一覧を取得。テンプレートに各値を渡します。

もう一つエントリーのポストを受け付けるエンドポイントを作りましょう。

post '/create' => sub {
    my ( $self, $c )  = @_;
    my $result = $c->req->validator([
        'body' => {
            rule => [
                ['NOT_NULL','empty body'],
            ],
        },
        'nickname' => {
            default => 'anonymous',
            rule => [
                ['NOT_NULL','empty nickname'],
            ],
        }
    ]);
    if ( $result->has_error ) {
        return $c->render_json({ error => 1, messages => $result->errors });
    }
    my $id = $self->add_entry(map {$result->valid($_)} qw/body nickname/);
    $c->render_json({ error => 0, location => $c->req->uri_for("/")->as_string });
};

POSTでデータを受け付け、JSONでレスポンスを返します。「/」と同じようにKossy::Validatorを使って入力値のチェックを行っています。JSONでレスポンスするには、$c->render_json が利用できますが、Amon2と同じ

  • 「GET リクエストである」かつ
  • 「Cookie ヘッダを送信している」かつ
  • 「/android/i にマッチする User-Agent ヘッダを付与している」かつ
  • 「X-Requested-With ヘッダを付与していない」

という条件に当てはまる場合、403エラーとなるので注意してください。

ここまででDispatcher側の変更はひとまず終了してテンプレートの編集に移ります。

テンプレートは、viewsディレクトリに配置し、Text::XslateのKolonシンタックスをサポートしています。index.txを開くと次のようになっています。

: cascade base
: around page_header -> {
<h1><: $c.stash.site_name :></h1>
: }

: around content -> {
<h2><: $greeting :></h2>
: }

base.txをカスケードしています。base.txにHTMLの構造を書き、各テンプレートにはコンテンツ部分だけを記す事ができます。index.txを編集してエントリー一覧を表示します。一覧の下には投稿用のフォームも付けます

: around content -> {
: for $entries -> $entry {
<h2><a href="<: $c.req.uri_for('/'~$entry.id) :>"><: $c.req.uri_for('/'~$entry.id) :></a></h2>
<pre class="prettyprint linenums:1">
<: $entry.body :>
</pre>
<p><: $entry.created_at :> by <: $entry.nickname :></p>
: }

<div class="pagination">
<ul>
: if $offset > 0 {
<li><a href="<: $c.req.uri_for('/',[offset => $offset-10 >= 0 ? $offset - 10 : 0]) :>">Prev</a></li>
: }
: if $has_next {
<li><a href="<: $c.req.uri_for('/',[offset => $offset+10]) :>">Next</a></li>
: }
</ul>
</div>

<form id="create-form" method="post" action="<: $c.req.uri_for('/create') :>">
<h2>New Paste</h2>

<div class="alert-message error hide">
<p>System Error! <em>try again</em>.</p>
</div>

<fieldset>
<div class="clearfix">
<label>body</label>
<div class="input">
<textarea class="span9" name="body" rows="10"></textarea>
<span class="help-block">fill your code</span>
</div>
</div>

<div class="clearfix">
<label>nickname</label>
<div class="input">
<input class="xlarge" name="nickname" />
<span class="help-block">your name</span>
</div>
</div>

<div class="actions">
<input type="submit" class="btn primary" value="Submit" />
</div>
</fieldset>
</form>
: }

最後に、投稿処理用のJavaScriptをbase.txに追加します。

<script type="text/javascript">
$(function(){
  $('#create-form').submit(function () {
    create_form = this;
    $.ajax({
      type: 'POST',
      url: create_form.action,
      data: $(create_form).serialize(),
      success: function(data) {
        $(create_form).find('.alert-message.error').hide();
        if ( data.error == 0 ) {
            location.href = data.location;
        }
        else {
          $(create_form).find('div.clearfix').removeClass('error');
          $.each(data.messages, function (param,message) {
            $(create_form).find('[name="'+param+'"]').
               parents('div.clearfix').first().addClass('error');
          });
        }
      },
      error: function() {
        $(create_form).find('.alert-message.error').show();
      },
      dataType: 'json'
    });
    return false;
  });
});
</script>

スケルトンの状態でjQueryは既に読み込まれています。

ここまでできたら、再度アプリケーションを起動します

$ plackup -r app.psgi

ブラウザで確認すると

kossy-sample.png

のようにエントリーの投稿もできると思います。画面はさらにgoogle-code-prettifyも追加してエントリの整形も行っています。

このようにKossyを使うと、Webアプリケーションを素早く開発できます。Sinatra風のDispatcherのため、大規模の開発には向きませんが、管理画面や運用ツールなどに試していただけるといいかなと思います

このブログ記事について

このページは、Masahiro Naganoが2011年12月24日 23:31に書いたブログ記事です。

ひとつ前のブログ記事は「レプリケーション作成を簡単にする mysql40dump という mysqldump の wrapper を作った話 」です。

次のブログ記事は「ImageMagickとOpenMPの件」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja