Mojolicious と DBIx::Skinny 使って jsonrpc 返すやつのオレオレ事例（テストもかいたよ）


Perl歴、webエンジニア歴、共に1年未満の俺が、何かしらのWebサービスを作ろうとしたときのパターン。

- 個人的なメモのつもりが長文になってしまった...

## で、なにこれ？


ゆーすけべーさんのMojoliciousのチュートリアルの[エントリ](http://yusukebe.com/archives/20120627/181253.html)を見て、
俺も似たような感じのものを作ってみようかな、ってのがモチベーション。

全く同じのもアレだし、MojoX::JSON::RPC::Service を利用して、
単にデータ出し入れできるようにしてみたチラシの裏です。

- 結果としてだけど、Nopaste 全然関係ないです
- 今回はperlだけで、js側 のコードはありません。そのうち追記できたら

ソースコード

- https://github.com/luckypool/mojo_skinny

# 動作例

```
# localhost:3000 で起動しとく
$ morbo script/mojo_skinny
```

```console:
# 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"}}
```

```console:
# 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"}}
```


# 実装内容のメモ


## ディレクトリ構成


まずはデフォルトの状態はこんな感じですね。

```console:
$ 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
```

現状は、こんなディレクトリ構成になった。

```console:
$ 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を作ることにしてみた。

```mysql:db-schema/DB_PRACTICE.sql
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 を使った諸設定を詰め込んである。

[MojoSkinny::DB](https://github.com/luckypool/mojo_skinny/tree/master/lib/MojoSkinny/DB)

- [MojoSkinny::DB::Config](https://github.com/luckypool/mojo_skinny/tree/master/lib/MojoSkinny/DB/Config.pm)
- [MojoSkinny::DB::Handler](https://github.com/luckypool/mojo_skinny/tree/master/lib/MojoSkinny/DB/Handler)
- [MojoSkinny::DB::Skinny](https://github.com/luckypool/mojo_skinny/tree/master/lib/MojoSkinny/DB/Skinny)

例えば、ModelからPractice DBに接続するときは、下記のようにHandlerを呼び出せば良いように実装してみた。


```perl:想定する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/*

[MojoSkinny::Model::Practice](https://github.com/luckypool/mojo_skinny/tree/master/lib/MojoSkinny/Model/Practice)

Modelでは insert, select, update, delete など基本的な操作が利用できるように実装してみた。
といっても、DBIx::Skinnyで基本は用意されているのでバリデーションつけただけに等しい。

```perl:想定するModelの使い方
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

[MojoSkinny::Web](https://github.com/luckypool/mojo_skinny/blob/master/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](https://github.com/luckypool/mojo_skinny/blob/master/lib/MojoSkinny/Web/JSONRPC/Practice/Entry.pm)


例えば、 lookup っていう method を生やす際は、下記のようになる。

- Modelの select_by_id を呼んで返すだけ
- Params::Validate して、ダメだったら、Mojo::Exception->throw する
- throw したら 上記で設定した *exception_handler* で、invalid_params の json が返る


```perl:
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](https://github.com/luckypool/mojo_skinny/blob/master/script/devel/proveit)
 - [t/proverc](https://github.com/luckypool/mojo_skinny/blob/master/t/proverc)

すると下記のようにテストファイルの実行できる

```console:
$ 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](https://github.com/luckypool/mojo_skinny/blob/master/lib/MojoSkinny/DB/Skinny/Base.pm) のような設定をしておいたので、デバッグ時に下記のような感じで確認できる

```perl:
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 使う

```perl:t/lib/MojoSkinny/Model/Practice/Entry.t
# Modelのテストを書くときは、下記を1行加えるだけでOK!
use MojoSkinny::Test::DB qw/DB_PRACTICE/;

```

- 上記のようにすれば、あとは普通にmodel使うだけでおｋ。内容は下記の通り

 - [Mojo::Skinny/Test::DB](https://github.com/luckypool/mojo_skinny/blob/master/lib/MojoSkinny/Test/DB.pm)
 - [Mojo::Skinny/Test::mysqld](https://github.com/luckypool/mojo_skinny/blob/master/lib/MojoSkinny/Test/mysqld.pm)

## jsonrpc のテスト

[01-crud.t](https://github.com/luckypool/mojo_skinny/blob/master/t/lib/MojoSkinny/Web/JSONRPC/Practice/Entry/01-crud.t)

基本は Test::Mojo を使って、下記の様にpostしてみる

```perl:
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 してみた

```perl:
my $expects = {
    jsonrpc => '2.0',
    id => 1,
    result => $expecting_json_hash_ref,
};
cmp_deeply $expects, $test_web->tx->res->json, q/json ok/;
```

# おわりに

個人的なメモのつもりが長文になってしまった...