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/;
おわりに
個人的なメモのつもりが長文になってしまった...