Help us understand the problem. What is going on with this article?

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

More than 5 years have passed since last update.

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

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

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

想定する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

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

想定する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

ルーティング設定

  • / にアクセスしたら、 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 使う

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

jsonrpc のテスト

01-crud.t

基本は 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/;

おわりに

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away