あと味

たくさん情報を食べて、たくさん発信すると、あとになって味わい深い。

PSGI + SeleniumでMTの管理画面のテストをする

MTの管理画面は、JavaScriptに依存した部分が結構多く、PhantomJS等のヘッドレスブラウザを使ったテストが必要になるケースがあります。

CasperJSや、Seleniumなど、いろいろなツールがありますが、MTの管理画面のロジックは、Perlで書かれているので、Perlで書けるのがベストです。Perlでデータの初期化とかしたいですからね。

Perlで書ける方法でツールを絞り込むと、現状では、Selenium::Remote::Driverを使うのが良い気がしています。

ということで、下記のようなテストを書いてみました。

テストの実行には、MTのGitHubリポジトリに含まれる、tディレクトリが必要です。あと、試す場合は環境変数MT_CONFIGを書き換えずに、MT::TestでDBを初期化すると、既存データが吹っ飛ぶので、テスト用の環境は別途用意した方が良いです。

Test::TCPで、GhostDriverと、MTのPSGIの管理画面を空いてるポートで起動して、Selenium::Remote::Driverを使って、管理画面を操作する感じです。

実際書いてみると、いろいろハマりどころがあった感じですが、ハマりどころを回避しつつテストを書くことになりそうです。

テストと同時にキャプチャを撮れるので、エビデンス対策も取れてよいですね。上記のコードではダッシュボードと、記事作成後の画面をキャプチャしていて、下記のような感じのキャプチャが撮れました。JavaScriptも処理されていて、ちゃんとした見た目になってます。

f:id:jdg:20140501090135p:plain

f:id:jdg:20140501090152p:plain

もう少し便利メソッドをカジュアルに使いたい場合は、Wight を使うのも良い気がします。

他言語でもSelenium使う機会があるかもしれないので、今後の学習コスト削減のために、今のところは単純なアダプタであるSelenium::Remote::Driverを使おうと思います。

ほなの。

MTオブジェクトの複製を作るツールスクリプトを作った

Movable Type使ってる時に、たまに、ページングのテストや負荷テスト等をしたい時に、記事を大量にコピーしたい時があって、これまで下記のようなスクリプトを叩いたりしてコピーを作ってました。

これをMTオブジェクト全般に広げても良いかなと思ったので、ObjectCloneというプラグインを作りました。

ObjectClone プラグイン

usage のままですが、下記のように使います。

USAGE: perl tools/object-clone --model=NAME --orig_id=NUM [OPTION]

Requires:
  -m, --model=NAME      Model name (object datasource) of object to make clone.
  -o, --orig_id=NUM     Original object id clone object.

Options:
  -a, --amount=NUM      Amount of clones to make. Default 1.
  -d, --debug           Output debug info to STDERR.
  -h, --help            Show help.
  -r, --redefine=PAIR   Redefine column value with column key and column value pair.
  -u, --usage           Show usage.

Examples:
  # Make clones of 100 objects of MT::Entry from original entry that entry_id is 1.
  $ tools/object-clone --model=entry --orig_id=1 --amount=100

  # Make a clone of MT::Blog, and redefine name, site_path and site_url.
  $ tools/object-clone -m blog -o 2 -r 'name=Clone Blog' -r 'site_path=/path/to/site-path' -r 'site_url=/::/clone-blog/'

基本は、モデル名とコピー元のオブジェクトのIDを、それぞれ、--modelオプションと--orig_id オプションで指定してクローンを作ります。

クローンを複数作る場合は、作る数を--amountオプションで指定します。

他には、単に丸々コピーするだけではなくて、一部、値を変更する場合は、--redefineオプションで、フィールド名とその値を=で区切って指定します。

具体的には、Aというブログの記事を、Bというブログにコピーしたいような時などに、下記のように指定します。

$ perl tools/object-clone --model entry --orig_id 1 --amount 10 --redefine blog_id=3

コマンドラインオプションの単語が適切かどうかは微妙な感じもしますが、とりあえず必要最低限なオプションだけ用意しました。

値の変更が必要なケース

DB上の制約はないにも関わらず、実際にはMT::Entryのベースネームだとか、CustomFields::Fieldのタグ名は、値が重複しないようにCMS上で制限がかかっていたり、バックグラウンドでの値の生成の実装があったりします。

その辺の対応はオブジェクト毎に必要な感じなので、ObjectClone::Patcherとそのサブクラスに切り出しています。

単体で使うことはほぼないと思いますが、下記のような感じで使います。

use strict;
use warnings;

use lib qw(lib extlib plugins/ObjectClone/lib);

use MT;
use ObjectClone::Patcher;
use ObjectClone::Patcher::MT::Entry;

my $mt = MT->instance;
my $orig_obj = MT::Entry->load(1) or die MT::Entry->errstr;
my $new_obj = $orig_obj->clone;
my $patcher = ObjectClone::Patcher->model('entry')->new($new_obj, $orig_obj);
$patcher->remove_patch(basename => \&ObjectClone::Patcher::MT::Entry::basename)
        ->add_patch(basename => sub { 'new-basename' })
        ->add_patch(convert_breaks => sub { 'markdown' })
        ->apply_patch;
$new_obj->save or die $new_obj->errstr;

デフォルトで下記のPatcherクラスを用意しています。

  • ObjectClone::Patcher::MT::Entry
  • ObjectClone::Patcher::MT::Category
  • ObjectClone::Patcher::MT::Author
  • ObjectClone::Patcher::CustomFields::Field

とは言っても実際に実装があるのは、ベースネームをユニークにする処理くらいです。

独自オブジェクトをtools/clone-objectで生成したいときなどは、上記のPatcherクラスを追加すれば対応ができます。( ObjectClone::Patcher::{オブジェクト名} )

後は、apply_patchメソッドで、パッチを適用する前に、before_apply_patchというコールバックが呼ばれるので、デフォルトのパッチ処理を無効にしたり、それに加えて何かの処理を実行したりする時は、コールバックプラグインで拡張できます。

コールバックプラグインのサンプル

下記のようにコールバックを使ったプラグインが書けます。

id: ObjectCloneCallback
key: object-clone-callback
name: ObjectCloneCallback
callbacks:
  ObjectClone::Patcher::MT::Entry::before_apply_patch: |
    sub {
      my ($cb, $patcher) = @_;
      $patcher->add_patch('basename', sub {
        my ($new_obj, $orig_obj) = @_;
        'new-basename';
      });
    }

まとめ

主に開発ツールとして作ったので、開発時には役に立つ気がしています。

シェルスクリプトと組み合わせたりするのも良いかもしれません。

ほなの。

MT::Object をワンライナーしやすくする拡張書いた

コマンドラインで、MT::Objectを取得して、加工して、出力してみたいなことすることが結構あるんですけど、MT::Objectは大変ワンライナーしにくい印象でした。

MTのシステム管理者のIDとパスワードがわからない時、コマンドで強制的に作ることがあるんですけど、MT::Objectがワンライナーしにくいので、下記のようなファイルを用意してから、実行するというまどろっこしさです。

試しにワンライナーしてみると下記のような感じでしょう。

$ perl -I{lib,extlib} -MMT -e 'my $taiju = MT->instance->model("author")->get_by_key({name=>"taiju"});$taiju->nickname("taiju");$taiju->email("higashi@taiju.info");$taiju->auth_type("MT");$taiju->status(1);$taiju->set_password("password");$taiju->is_superuser(1);$taiju->save or $taiju->errstr;printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", $taiju->id;'

...セミコロン......

これがワンライナーと言えるなら、minifyされた jQueryですらワンライナーになってしまいますね。

MT::Object::Chaining

ということで作ったのが下記になります。

MT::Object::Chaining - Methods chaining for MT::Object

MT::Object::Chainingを使うと、下記のように書けます。

perlコマンドのように、Iオプションや、Mオプション指定して、インスタンス取得するのすらダルいので、tools/chainというスクリプトでその辺をカバーしてます。

mオプションでモデルを指定して、eオプションでeval文字列を指定します。

$ cd /path/to/mt && tools/chain -m author -e '$model->get_by_key({ name => "taiju" })->nickname("taiju")->email("higashi\@taiju.info")->auth_type("MT")->status(1)->set_password("password")->is_superuser(1)->save->tap(sub { printf "You can use taiju (id=%d) (password=password)! taiju is suupppeeeer user!!\n", shift->id })'

こんな感じでメソッドチェーンで書けるようにする機能を提供するような感じですね。

まとめ

これでMT::Objectをワンライナーしやすくなりました。

jsonで吐くAPIなども用意しているので、

$ tools/chain -m author -e '$model->load->json' | jq '.[] | {name, email}'

みたいな感じでjqに渡したりなど、捗るかもしれません。

また、mapメソッドで一括置換して保存とか、reduceで加工した結果を出力するとか、普段使いに個人的に便利だと思う機能も入れました。

まだ、v0.1なのと、統一感に欠ける気もしているので、インターフェイスの変更するかもしれません。

詳しい使い方は、README.pod を参照下さい。

https://github.com/taiju/MT-Object-Chaining/blob/master/README.pod

ほなの。

MT アプリケーションを Sinatra like に記述できる MT::App::Lite 作った

Movable Type には、MT::App という Web アプリケーションを記述するための基底クラスがありますが、結構慣れが必要なのと癖があることもあって、もっと手軽に Sinatra like にアプリケーションを記述できる、フレームワーク的なものが欲しいなーと常々思っていました。

とりあえずいろいろやりたいことは残っていますが、動くところまでできたので、紹介します。

現状では、PSGI でのみ動作します。

名前

概要

以下のような感じでMTアプリケーションを記述します。

package MyLiteApp;
use strict;

use MT::App::Lite;

setup Renderer => 'Xslate';

get '/' => sub {
  my $app = shift;
  $app->render('index.tt', {
    blog => MT->model('blog')->load(1),
    entries => [MT->model('entry')->load({blog_id => 1})],
  });
};

get '/entry/:id' => sub {
  my $app = shift;
  $app->render('entry.tt', {
    blog => MT->model('blog')->load(1),
    entry => [MT->model('entry')->load($app->param('id'))],
  });
};

1;

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $blog.name :></title>
</head>
<body>
  <ul>
  : for $entries -> $entry {
    <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li>
  : }
  </ul>
</body>
</html>

@@ entry.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $entry.title :> | <: $blog.name :></title>
</head>
<body>
  <h1><: $entry.title :></h1>
  <div>
    <: $entry.text | mark_raw :>
  </div>
</body>
</html>

1ファイル完結型です。(正確にはプラグインの config.yaml が必要)
多分、フォームとか作るのに便利だと思います。

仕様

MT アプリケーションのハンドラ内で、Router::Simple::Sinatraish でエクスポートされる、get や post などの関数を使って、ルートを記述します。

そのファイル中の __END__ セクションに、テンプレートを書きます。
__END__ セクションに記述したテンプレートは、Data::Section::Simple によって、MT::App::Lite に渡されます。

テンプレートは、Text::Xslate を標準のテンプレートエンジンにしています。申し訳程度に MTML 用のテンプレートエンジンも用意しましたが、ここでわざわざ使う必要はない気がします。

MT::App::Lite::Renderer::Foo という形で、render メソッドと render_string メソッドを用意すれば、任意のテンプレートエンジンを利用することは可能です。

また、MT::App::Lite を use すると、自動的に MT::App を継承することになるので、MT::App っぽい書き方もできるのかもしれません。

基本的には、MT::App に Web アプリケーションを記述するための機能はひと通り揃っているので、便利っぽいモジュールを糊付けしたような実装です。

MT アプリケーションのプラグインを作成する

このモジュールを使った、MTアプリケーションのプラグインの作り方です。

最小限の config.yaml と、ハンドラを用意します。

config.yaml
name: MyLiteApp
id:   myliteapp

applications:
  lite_app:
    handler: MyLiteApp
    script: sub { 'app' }
    cgi_path: sub { '/' }
MyLiteApp (handler)
package MyLiteApp;
use strict;

use MT::App::Lite;

setup Renderer => 'Xslate';

get '/' => sub {
  my $app = shift;
  $app->render('index.tt', {
    blog => MT->model('blog')->load(1),
    entries => [MT->model('entry')->load({blog_id => 1})],
  });
};

get '/entry/:id' => sub {
  my $app = shift;
  $app->render('entry.tt', {
    blog => MT->model('blog')->load(1),
    entry => [MT->model('entry')->load($app->param('id'))],
  });
};

1;

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $blog.name :></title>
</head>
<body>
  <ul>
  : for $entries -> $entry {
    <li><a href="<: $entry.permalink :>"><: $entry.title :></a></li>
  : }
  </ul>
</body>
</html>

@@ entry.tt
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title><: $entry.title :> | <: $blog.name :></title>
</head>
<body>
  <h1><: $entry.title :></h1>
  <div>
    <: $entry.text | mark_raw :>
  </div>
</body>
</html>

これで、http://yourdomain/app/ や、http://yourdomain/app/entry/:id にアクセスするとテンプレートの内容が描画されます。

まとめ

本当は、Movable Type Advent Calendar 2013 の自分担当日のネタとして作ろうと考えていたんですけど、実装する暇がなく、今日に至ります。

便利な気がするので、メンテしていこうと思います。まだいろいろと実装したいことや、実装すべきことがあります。

現時点ではちょっとした Viewer やフォームを作るのには便利な気がしています。

Ruby の method_missing 的な tag_missing コールバックを MT に追加してみる

Movable Type Advent Calendar 2013 の8日目です。

Rubymethod_missing だったり、PerlAUTOLOAD 的なものが、MT のタグを利用する時に使えると便利かなと思って、TagMissingプラグインなるものを作ってみました。存在しないタグを利用しようとした時に、tag_missing というコールバックを呼ぶ単純なものです。ダイナミックは未対応です。

ただ、これを実現する方法は、MT の処理をモンキーパッチするしかなさそうだったのと、作ってみたものの、インターフェイスも微妙な感じがしますが、せっかくなので公開します。

TagMissing プラグイン

タグが存在しない時のエラー処理は、MT::Builder::build を見る限り、下記のあたりにあって、

            my $hdlr = $ctx->handler_for( $t->tag );
            my ( $h, $type, $orig ) = $hdlr->values;
            my $conditional = defined $type && $type == 2;

            if ($h) {
# ... 省略 ...
            }
            else {
                if ( $t->tag !~ m/^_/ ) {    # placeholder tag. just ignore
                    return $build->error(
                        MT->translate( "Unknown tag found: [_1]", $t->tag ) );
                }
            }

https://github.com/movabletype/movabletype/blob/de8fc5aba805a113075aa7e58882855554dd5d5a/lib/MT/Builder.pm#L475-L591

MT::Template::Context::handler_for メソッドの結果次第で、分岐の経路が変わる感じなので、MT::Template::Context::handler_for の中にコールバックを仕込むのが確実かなと思ったので、TagMissing プラグインでは、handler_for をパッチする方針にしました。

モンキーパッチする時は、本体のバージョンアップの影響をなるべく受けないようにした方が良いので、無駄が多い感じですが、handler_forをコールバック前後で2回呼び出す形にしました。

package TagMissing::Patch;

use strict;
use warnings;

sub init {}

use MT::Template::Context;

my $orig_handler_for = \&MT::Template::Context::handler_for;

{
  no warnings 'redefine';
  *MT::Template::Context::handler_for = sub {
    my $ctx = shift;
    my $tag = lc $_[0];
  
    my $hdlr = $orig_handler_for->( $ctx, $tag );
    my ( $h ) = $hdlr->values;
  
    MT->run_callbacks( 'tag_missing', $ctx, $tag ) unless $h;

    $orig_handler_for->( $ctx, $tag )
  
  };
}

1;

https://github.com/taiju/mt-plugin-TagMissing/blob/8b69c4f3c6f372de7de032b68260c042a4b29eea/plugins/TagMissing/lib/TagMissing/Patch.pm

インターフェイス的にかなり微妙な感じですが、tag_missing コールバックの中で、$ctx->{__handlers}{tag_name_foo}に MT::Templage::Handler 形式でハンドラを代入するなどして、タグの動作を動的に指定することができます。

AutoUnless プラグイン

そんな感じで、tag_missing コールバックを使ったタグのリファレンス実装として、AutoUnless プラグインというのも作ってみました。

mt:IfWebsite みたいな、If という単語の含まれたタグがいろいろありますが、mt:UnlessWebsite みたいな逆の意味を持つタグがあったりなかったりするので、mt:*unless*がなければ自動的に定義するプラグインです。

tag_missing が呼ばれた時に、動的に定義する感じの実装で、tag_missing コールバックのサンプルプラグインにもなっています。

MT の t ディレクトリにある、35-tags.t を丸々移して、mt:*If*mt:*Unless* に書き換えた上で、テスト結果を逆にしたテストを書いてみましたが、一部を除いてうまく動いているようです。

https://github.com/taiju/mt-plugin-AutoUnless/blob/master/plugins/AutoUnless/t/tags.t

動的にいくつかのタグを定義するような時には、tag_missing コールバックは便利に使えそうです。

まとめ

あると便利な気はするんですけど、アイデアありきで作ってみたものの、実装は正直微妙な感じになってしまいました。

今年はコードを書くより、読む時間が圧倒的に多い年でした。来年は、書く時間を増やせたらいいなと思います。

Perlでメソッドチェーンのメソッドを動的に決定する

Perl でメソッドを呼び出す時、

$class->$method;

みたいな呼び出し方できるのは知ってたけど、

$class->${ \'method' }

みたいな感じで、文字列のスカラーリファレンスをデリファレンスすることでも同じことができることを知った。

これを用いることでメソッドチェーンのメソッドを動的に決定することができる。

上記のように pass とかいうメソッドを用意しておけば、不要なメソッドはスキップするということもできるので、そのチェインを実行するかどうかも動的に決定できる。

コードは結構キモい感じだけど、それなりに実用性はありそう。

MTDDC 2013の感想文

感想文書くの遅くなりましたが、先日、MTDDC 2013に参加しました。

LTについて

Data APIをさわるキッカケを作りたいなと思って、さわる前にLT応募したんですけど、実際にさわってみるのはギリギリになってしまって、焦ってたんですが、5分だったのでなんとかなりました。

とりあえず、できることを確認して、サンプルアプリ作ってみて、ファーストインプレッションをまとめた感じです。

下記にLTのスライドを置いておきました。

LT用に作ったサンプルアプリケーション

Movable Type Data APIは、結構ポテンシャルが高いなと思っていて、スライドにも書きましたが、MTの管理画面を作るだけじゃなくて、JSONを使ったフロントエンドアプリが普通に作りやすくなりますねってことを紹介するために、ToDoアプリケーションを作りました。

とは言え、作ったというと大げさで、下記の記事にあるチュートリアルそのままで、CoffeeScript + Data APIで少し書き直しただけです。

作ったサンプルは下記に置いておきました。

Data APIはURLの設計キレイだし、JSON/REST APIなので、Backbone.jsとの親和性がとても高いと思いました。バックエンドはMTをセットアップしただけで利用できるので、Backbone.jsでフロントエンドのアプリを書くだけで済みます。アプリのプロトタイプ作るのに活用するのもいいなと思いました。

後、フロントエンドの人達も活用しやすいと思います。フロントエンド技術だけでも、Data APIを使って、Webアプリを作れるとおもいますし、DBのデータの管理アプリケーションにMTが使えます。

MT6について

やはりData APIに注目が集まっているようです。

自分もData APIは、MTの静的ファイル出力やMTML、Perlなど、MTのアイデンティティになる要素を一切排除できる仕組みなので、かなり大胆な機能追加だなと思いました。

今後Data APIがどのような使い方をされていくのか楽しみではあります。

あと、長谷川恭久さんを中心にMTがどのようにリデザインされていくのかも注目です。

新しいMTはSimple, Smart, Speedyがキーワードとのことですが、今のMTは残念ながらどのキーワードもしっくりとマッチしていないように思うので、これからそのキーワード通りに変化していくことを期待しています。