Perl歴、webエンジニア歴、共に1年未満の俺が、何かしらのWebサービスを作ろうとしたときのパターン。
- 個人的なメモのつもりが長文になってしまった...
で、なにこれ?
ゆーすけべーさんのMojoliciousのチュートリアルのエントリを見て、
俺も似たような感じのものを作ってみようかな、ってのがモチベーション。
全く同じのもアレだし、MojoX::JSON::RPC::Service を利用して、
単にデータ出し入れできるようにしてみたチラシの裏です。
- 結果としてだけど、Nopaste 全然関係ないです
- 今回はperlだけで、js側 のコードはありません。そのうち追記できたら
ソースコード
動作例
# localhost:3000 で起動しとく
$ morbo script/mojo_skinny
# create
$ curl -X POST http://localhost:3000/jsonrpc/practice/entry.json -d '{"jsonrpc": "2.0", "method": "create", "params": { "nickname": " おれおれ!", "body": "ぼでぃー"}, "id": 1}'
# response
{"jsonrpc":"2.0","id":1,"result":{"body":"ぼでぃー","created_at":"2013-03-03 22:57:27","nickname":"おれおれ!","id":"2"}}
# lookup
$ curl -X POST http://localhost:3000/jsonrpc/practice/entry.json -d '{"jsonrpc": "2.0", "method": "lookup", "params": { "id": 2 }, "id": 1}'
# response 
{"jsonrpc":"2.0","id":1,"result":{"body":"ぼでぃー","created_at":"2013-03-03 22:57:27","nickname":"おれおれ!","updated_at":"0000-00-00 00:00:00","id":"2"}}
実装内容のメモ
ディレクトリ構成
まずはデフォルトの状態はこんな感じですね。
$ mojo generate app MojoSkinny
$ tree mojo_skinny/
mojo_skinny/
├── lib
│   ├── MojoSkinny
│   │   └── Example.pm
│   └── MojoSkinny.pm
├── log
├── public
│   └── index.html
├── script
│   └── mojo_skinny
├── t
│   └── basic.t
└── templates
    ├── example
    │   └── welcome.html.ep
    └── layouts
        └── default.html.ep
現状は、こんなディレクトリ構成になった。
$ tree mojo_skinny/
mojo_skinny
├── README.md
├── db-schema
│   └── DB_PRACTICE.sql
├── lib
│   └── MojoSkinny
│       ├── DB
│       │   ├── Config.pm
│       │   ├── Handler
│       │   │   ├── Base.pm
│       │   │   └── Practice.pm
│       │   └── Skinny
│       │       ├── Base.pm
│       │       ├── Practice
│       │       │   └── Schema.pm
│       │       └── Practice.pm
│       ├── Model
│       │   └── Practice
│       │       ├── Base.pm
│       │       └── Entry.pm
│       ├── Test
│       │   ├── DB.pm
│       │   └── mysqld.pm
│       ├── Web
│       │   ├── JSONRPC
│       │   │   └── Practice
│       │   │       └── Entry.pm
│       │   └── Root.pm
│       └── Web.pm
├── public
│   ├── (省略)
├── script
│   ├── devel
│   │   └── proveit
│   └── mojo_skinny
├── t
│   ├── 99-perlcritic.t
│   ├── lib
│   │   └── MojoSkinny
│   │       ├── DB
│   │       │   ├── Config.t
│   │       │   ├── Handler
│   │       │   │   ├── Base.t
│   │       │   │   └── Practice.t
│   │       │   └── Skinny
│   │       │       ├── Base.t
│   │       │       ├── Practice
│   │       │       │   └── Schema.t
│   │       │       └── Practice.t
│   │       ├── Model
│   │       │   └── Practice
│   │       │       ├── Base.t
│   │       │       └── Entry.t
│   │       ├── Test
│   │       │   ├── DB.t
│   │       │   └── mysqld.t
│   │       ├── Web
│   │       │   ├── JSONRPC
│   │       │   │   └── Practice
│   │       │   │       └── Entry
│   │       │   │           ├── 00-use_ok.t
│   │       │   │           ├── 01-crud.t
│   │       │   │           └── 02-find.t
│   │       │   └── Root
│   │       │       ├── 00-use_ok.t
│   │       │       └── 01-basic.t
│   │       └── Web.t
│   ├── perlcriticrc
│   └── proverc
└── templates
    ├── layouts
    │   └── root
    │       └── default.html.ep
    └── root
        └── home.html.ep
db-shema
まずは何かしらのデータを jsonrpc で返すとして、適当にこんなDBを作ることにしてみた。
DROP DATABASE if EXISTS practice;
CREATE DATABASE practice;
USE practice;
DROP TABLE if EXISTS entry;
CREATE TABLE entry (
    id INT unsigned NOT NULL AUTO_INCREMENT,
    nickname VARCHAR(32) NOT NULL,
    body VARCHAR(255) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT 0,
    updated_at DATETIME NOT NULL DEFAULT 0,
    PRIMARY KEY (id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
名前空間のメモ
- DB_***.sql としたときは、*** が データベースの名前
- つまり、上記の内容は、 DB_PRACTICE.sql
- ここにテーブルの構成とか書いとく
- 後でこのsqlファイルをテスト時に読み込めるように
lib/MojoSkinny/DB/*
DBIx::Skinny を使った諸設定を詰め込んである。
例えば、ModelからPractice DBに接続するときは、下記のようにHandlerを呼び出せば良いように実装してみた。
my $master = MojoSkinny::DB::Handler::Practice->new(role=>'m')->db;
my $slave = MojoSkinny::DB::Handler::Practice->new(role=>'s')->db;
# 単にmysqlにつなぐときのユーザー名を分けてるだけなので、なんちゃって master/slave
# あとはDBIx::Skinny を使うときと同じ
$master->insert('entry', {
	nickname => 'hogehoge',
	body => 'fugafuga',
});
名前空間のメモ
- 
ひとつの DB につき、下記が対応するようにした 
- 
MojoSkinny::DB::Handler::XXX ← 対応する Skinny を new して返すだけ 
- 
MojoSkinny::DB::Skinny::XXX 
- 
MojoSkinny::DB::Skinny::XXX::Schema 
- 
MojoSkinny::DB::Config には 'DBI:mysql:xxx' を呼べるようにしとく 
- 
もし DB practice に teble が増えたら、下記を修正することになる 
- 
MojoSkinny::DB::Skinny::Practice::Schema 
- 
もし hoge という DB が増えたら、下記が増えることになる 
- 
MojoSkinny::DB::Handler::Hoge 
- 
MojoSkinny::DB::Skinny::Hoge 
- 
MojoSkinny::DB::Skinny::Hoge::Schema 
- 
MojoSkinny::DB::Config の定数 'DBI:mysql:hoge' 
lib/MojoSkinny/Model/*
Modelでは insert, select, update, delete など基本的な操作が利用できるように実装してみた。
といっても、DBIx::Skinnyで基本は用意されているのでバリデーションつけただけに等しい。
my $model = MojoSkinny::Model::Practice::Entry->new;
my $row = $model->select_by_id({id=>$id});
# Model の中では、 Params::Validate を使って引数チェックしている
名前空間のメモ
- ひとつの テーブル につき、ひとつの Model が対応するようにした
- practice という DB に entry というテーブルがあるので、 そのテーブルを扱う Model は、 MojoSkinny::Model::Practice::Entry となる
- もしテーブルが増えたら、MojoSkinny::Model::Practice::Hoge をつくることになる
lib/MojoSkinny/Web.pm
ルーティング設定
- / にアクセスしたら、 Root の home を呼ぶ
- /jsonrpc/practice/entry.json の設定を plugin に
- MojoX::JSON::RPC::Service を使ってます
- post されたパラメーターが、バリデーションにパスしなかったら die することにして、その際のエラーハンドラーも書いておく
lib/MojoSkinny/Web/*
ルーティングしたあとの中身
名前空間のメモ
- urlのパスに対応するようにしとく
- /jsonrpc/practice/entry.json の中身はこれ → lib/MojoSkinny/WEB/JSONRPC/Practice/Entry.pm
lib/MojoSkinny/Web/JSONRPC/Practice/Entry.pm
MojoSkinny::Web::JSONRPC::Practice::Entry
例えば、 lookup っていう method を生やす際は、下記のようになる。
- Modelの select_by_id を呼んで返すだけ
- Params::Validate して、ダメだったら、Mojo::Exception->throw する
- throw したら 上記で設定した exception_handler で、invalid_params の json が返る
package MojoSkinny::Web::JSONRPC::Practice::Entry;
use strict;
use warnings;
use utf8;
use Mojo::Base 'MojoX::JSON::RPC::Service';
use Mojo::Exception;
use Params::Validate;
use MojoSkinny::Model::Practice::Entry;
__PACKAGE__->register_rpc_method_names( 'lookup' );
sub lookup {
    my $self = shift;
    my $params = __validate_id(@_);
    my $model = MojoSkinny::Model::Practice::Entry->new;
    return $model->select_by_id($params);
}
sub __validate_id {
    return Params::Validate::validate_with(
        params => @_,
        spec =>  {
            id => {
                type    => Params::Validate::SCALAR,
                regex   => qr/^\d{1,5}$/,
            },
        },
        on_fail => sub { __throw(@_) },
    );
}
sub __throw {
    my $message = shift;
    Mojo::Exception->throw([$message]);
}
1;
テストの事情
script/devel/proveit
prove つかってテストしたい。ってことで、下記のようなファイルをつくる
すると下記のようにテストファイルの実行できる
$ script/devel/proveit t/lib/MojoSkinny/Model/Practice/Base.t 
[21:58:41] t/lib/MojoSkinny/Model/Practice/Base.t .. 
ok 1 - use MojoSkinny::Model::Practice::Base;
1..1
ok      192 ms
[21:58:42]
All tests successful.
Files=1, Tests=1,  1 wallclock secs ( 0.03 usr  0.01 sys +  0.18 cusr  0.01 csys =  0.23 CPU)
Result: PASS
# ディレクトリ内の複数ファイルの実行は下記でできる
$ script/devel/proveit t/lib/MojoSkinny/Model
# …省略
SKINNY_PROFILE=1
prove の前に ↑これを設定しておくと、query_logみれる
実は、MojoSkinny::DB::Skinny::Base のような設定をしておいたので、デバッグ時に下記のような感じで確認できる
warn Data::Dumper::Dumper $model->master->query_log
t/lib/MojoSkinny/
名前空間のメモ
- lib/MojoSkinny 以下のモジュールに対応して、 t/lib/MojoSkinny にテストコードを配置
- テスト書くのが面倒だとしても、せめて use_ok はしておく ← これだけでも凡ミス検出してくれるので、結構大事
lib/MojoSkinny/Test/DB.pm と lib/MojoSkinny/Test/mysqld.pm
テスト実行時に DB 接続したくない → Test::mysqld 使う
# Modelのテストを書くときは、下記を1行加えるだけでOK!
use MojoSkinny::Test::DB qw/DB_PRACTICE/;
- 
上記のようにすれば、あとは普通にmodel使うだけでおk。内容は下記の通り 
jsonrpc のテスト
基本は Test::Mojo を使って、下記の様にpostしてみる
my $test_web = Test::Mojo->new('MojoSkinny::Web');
$test_web->post_ok(
        '/jsonrpc/practice/entry.json',
        json => {
            jsonrpc => '2.0',
            method  => $method,
            params  => $params,
            id => 1,
        })
    ->status_is(200)
    ->content_type_is('application/json-rpc')
    ->header_is('X-Powered-By' => 'Mojolicious (Perl)');
返ってきたjsonの検証は、Test::Deep使って cmp_deeply してみた
my $expects = {
    jsonrpc => '2.0',
    id => 1,
    result => $expecting_json_hash_ref,
};
cmp_deeply $expects, $test_web->tx->res->json, q/json ok/;
おわりに
個人的なメモのつもりが長文になってしまった...
