taiyoh's memorandum

@ttaiyoh が、技術ネタで気づいたことを書き溜めておきます。

Amon2のDispatcher::RouterSimple、個人的にこんなのだといいなー、という例

[追記]
tokuhirom氏に「よさげ」って言ってもらえたので、調子にのってgithubに上げました(シーパン王サーじゃないので)。Amon2::Web::Dispatcher::RouterSimple::Extendedって名前にしております。::RailsLikeとかも考えたけど、関数名違うから混乱するのでやめ。そして自分の英語がひどすぎて死ぬ。あとモジュール名長すぎる。
https://github.com/taiyoh/p5-Amon2-Web-Dispatcher-RouterSimple-Extended
[追記終わり]
[追記2]
gfx氏から「no strict 'refs';状態でガリガリ書くのはマズイ」という指摘を受けて、確かにそうだよなー、でも、submapperへの出入りでコロコロ切り替わるから、限定するのはムズいよなー、と思ったので、関数はある程度他所で定義するようにして、あとは力技でuse strict 'refs'をこまめに入れて厳密性を高めるようにしてみた。ちょっと僕の技量だとこれが限界なので、もっといい実装あればアドバイス or pull-reqください。。。><
https://github.com/taiyoh/p5-Amon2-Web-Dispatcher-RouterSimple-Extended/commit/8fd8e02cfb31bfb852644f163aeae24f251a85c9
[追記2終わり]

 久々にPerl書いております。
 Amon2でやってみようかと思って色々読んでいるのですが、Router周りはRails的な方が個人的には好みだなー、と思ったので、こんな風に変えてみた。一応テストも書いて通している。

 こうすると、

package MyApp::Web::Dispatcher;
use strict;
use warnings;
use utf8;

use Amon2::Web::Dispatcher::RouterSimple;

connect '/'    => 'Root#index';

# API
submapper '/api/' => API => sub {
    get  'foo' => 'foo';
    post 'bar' => 'bar';
};

# user
submapper '/user/' => User => sub {
    get     '',           'index';
    connect '{uid}',      'show';
    post    '{uid}/hoge', 'hoge';
    connect 'new',        'create';
};

1;

 という感じで、色々関数を使えるようになる。最初はsubmapperの扱いがぞんざいだと思ったので、再定義して第三引数に定義した関数内もconnectを使えるようにしただけだったんだけど、欲が出たのでget/post/put/deleteも使えるようにしてしまった。
 あとホントはantipop氏が1年前くらいのブログ記事で書いている通り、

ディスパッチされるコントローラのクラスが、::C::*というような名前で、僕はクラス名などを省略するのが個人的に嫌い

ぼくがかんがえたさいきょうのAmon2のつかいかた - delirious thoughts

 というのに僕も賛成なのですが、まあ、もういいや、と。自分でRouterを再定義してそっちを使うようにすればいいので。

ともあれ、Amon2はわかりやすく、かつ、カスタマイザブルなWAFなので、とても使い易いものだと思います。これでちょっといろいろ試してみたいところです。

ぼくがかんがえたさいきょうのAmon2のつかいかた - delirious thoughts

 ということなので、Amon2のコア部分とうまく付き合いながら、オレオレになりすぎないようにやっていこうと思っております。

結果だけじゃなくてプロセスも大切なんだよ、ってお話

「手段がどうかじゃなくて最終的に何をゴールにするかだよね」っていう言説をよく見る気がしていて、まあそれは真理だよねと思いつつ、どこかでずっと引っ掛かってたけど、やっとそこが言葉に出来た気がする。

一番気になっていたのは、上記の言葉がどういう文脈で語られているかという点だった。一般的には目的を論じる場なのに手段に偏った意見が出てきた時にカウンターとして使うけど、たまに、自称俺は目的やゴールが大切ですから細かいことは気にしません君が相手の意見を封じる際に使ったりもしてて、これが厄介なんだな、と。

端的に結論を言うと、僕があるべきと思う文脈は、
「目的やゴールが大切なんだから、手段とかどうでもいいんだよ」じゃなくて、
「手段は大切なんだけど、それに拘ってるとゴールを見失うから、常に気をつけていましょうね」って方だな。
本当にゴールに拘る人はそもそももっと大きなスケールで話をするし、
プロセスと結果の二元論で議論になるレベルの問題だったら、そもそもどっちがプライマリかとか、いちいち話す時間が勿体ない。

余談だけど、もっと抽象的にその先を話すと、プロセスを通してゴールに導く人も、ゴールを見定めて導く人もどちらも大切で、お互いに自分の役割に誇りを持ちつつ、もう片方をリスペクト出来るのがいいなあ、と僕は思う。プロセスを考えないといけない人が、ゴールにばかり目を奪われて自分の役割を疎かにしているとか、ゴールを見定めて導く人が、そのためのプロセスについて無関心だとか、そういうディスコミニュケーションは不幸の始まりなんじゃないかな、と。

更に煙に巻くことを書いておくと、ここは戦略と戦術の関係について話をしてるんじゃなくて、因果の話をしていますよ、と。

node.jsのvmとsynchronizeはライフチェンジング

[追記 5/29]
 synchronizeというかfiberには以下の問題点があるのでそちらを参照の上、このエントリをご覧ください
 → node-fiberでライフチェンジングとか煽ったことを若干後悔してる - taiyoh's memorandum
[追記 終わり]

 node.jsをお使いのみなみなさま、コールバック地獄の中をいかがお過ごしでしょうか。
 最近になって僕はvmモジュールとsynchronizeモジュールを使い出しまして、これがちょっと尋常じゃないくらい自分の実装方法に影響を与えております。
 百聞は一見にしかずということで、実際どんな感じで使っているか、最近作ったユーティリティファイルの一部を載せます。

// util.js
var vm   = require('vm')
  , fs   = require("fs")
  , path = require("path")
  , sync = require("synchronize")
  , root = path.resolve(__dirname + "/../");
// rootは、実行するアプリケーションのルートディレクトリが指定できればいい

// fs.readFileメソッドを"同期"処理にする!!!
sync(fs, "readFile");

module.exports = {
  runScript : (function() {
    var cache = {};
    return function(p, context) {
      if (!cache[p]) {
        cache[p] = fs.readFile(root + "/" + p, "utf-8");
      }
      vm.runInContext(cache[p], this.createContext(context), p);
    };
  })(),
  createContext : function(context) {
    context = context || {};
    context.require = require;
    context.module  = module;
    context.console = console;
    context.global  = global;
    context.myutil  = this;
    context.sync    = sync;
    return vm.createContext(context);
  }
}

これを実行ファイルか何かで

var myutil = require("./lib/util");

という感じで受け取っておき、アプリケーションに必要なコードは全てmyutil.runScript(ファイル名, コンテキストになるオブジェクト)でロードするルールにしてます。
vm.runInContextの一番の使いドコロは、やっぱり中で実行する処理のコンテキストを自分で指定できるのがよくて、globalを汚さずに済むし、時々DSLというか黒魔術っぽいこともできるあたりで、ファイル毎に色を持たせてシンプルに記述することができます。あと、vm.runInContextの第一引数には実際の処理を文字列として入れなくちゃいけなくてすげーめんどいので、runScript関数としてファイルのロード+コードのキャッシュまで全部面倒みるようにしてるのもポイント。
 そしてsynchronizeは、非同期処理のはずなのに同期処理っぽく書けるようになる夢のようなモジュールです。コアにはFiberというモジュールを使っていて、このFiberというのは、Fiberのブロック内で特殊なフィールドを作り出し、イベントループは続いてるはずなのに、実際の処理はFiber.yield()で値を返さないと続きが実行されない、というよくわかんない状態になります。
laverdet/node-fibers · GitHub
FiberのReadMeにサンプルがいくつもあるので、動かしてみてくださいな。
とにかく、要はsynchronizeというのは、Fiberをもっと気軽に使えるようにするラッパーということです。
 例えば、さっきのutil.jsで

sync(fs, "readFile");

とありますが、内部的にはおおよそこんな感じのことをしています。
ここでは概念的に書くだけなので、実際のコードはもう少し複雑です。

var original_readFile = fs.readFile;
fs.readFile = function(filename, encode, callback) {
  if (typeof callback == 'function') {
    original_readFile.apply(fs, arguments);
  }
  else {
    encode = encode || "utf8";
    return sync.await(original_readFile.call(fs, filename, encode, sync.defer()));
  }
};

さてここで新たにawaitとdeferというメソッドが出て来ました。sync.deferメソッドは、大雑把に書くとこんなことをしてます。

sync.defer = function() {
  var fib = Fiber.current;
  return function(err, result) {
    if (err) {
      throw err;
    }
    fib.run(result);
  };
};

これを、sync.await(= Fiber.yield)で待って結果を返す、というのが、synchronizeで同期っぽく見せるカラクリです。
 ここで注意なのが、sync.defer()で返ってくるコールバック関数は、必ず第一引数にエラー、第二引数に結果オブジェクト、というルールになっているので、適当に第一引数に結果を渡すような実装にsync.defer()を混ぜてしまうと、かなり泣きを見ることになります。
 これを応用して、DB周りの実装でsynchronizeを使うと、ライフチェンジング度がもっとアップします。今Sequelizeを使っているのですが、モデル定義の際、classMethodsに以下の様な処理を入れてみました。

opts.classMethods = {
  findAwait: function(attrs) {
    var cb = sync.defer();
    return sync.await(
        this.find(attrs)
            .success(function(obj) { cb(null, obj); })
            .error(function(err) { cb(err, null); })
    );
  },
  findAllAwait: function(attrs) {
    var cb = sync.defer();
    return sync.await(
        this.findAll(attrs)
            .success(function(results) { cb(null, results); })
            .error(function(err) { cb(err, null); })
    );
  }
};

そうすると、今までは

User.find({where:{foo:"bar"}}).success(function(user) {
  // コールバックで続きの処理を書かなくてはいけない
});

だったのが、

var user = User.findAwait({where:{foo:"bar"}});

と書けるようになるので、これなら同期処理の書き方と殆ど変わらない!!!ほんの2ヶ月くらい前だったらjQuery.Deferredを使ってましたが、このsynchronize(=Fiber)の旨味を知っちゃったら、もう昔には戻れないっすね。。。
 もちろん、こうした書き方はsync.fiber()(というかFiber(function() {}).run())の中でしか動かないという制約はあるので、実装は多少変更にはなるのですが、その変更部分をなるべく最小限に抑えられれば、この余りあるメリットを存分に享受できるのではないでしょうか。
 あと欲を言えば、Fiberかsynchronizeには、concurrencyもサポートしてもらえたら最高ですね。そうすると、RubyEM-Synchronyっぽいことができるようになるなぁ、と。

ミサワのページから画像をブッコ抜きつつmarkdownのテキストも出力するchrome拡張書いた

taiyoh/misawa-markdown · GitHub
やっぱり使いたい画像で即座にミサワ画像使いたいよねー、というのと、
chromeの拡張書いたことなかったので、試しにやってみよう、ということで作ってみました。

f:id:sun-basix:20121009212020p:plain
↑使用イメージ

ストアとか載せるの面倒臭いし、多分もっと色々できることある気がするので、
もし追加機能あれば勝手にforkして使ってください。
使い勝手云々とかも色々ありそうだけど、当初の目的を果たしたのでもう飽きた。
JSのコード汚いのも分かるけど、もういいや><

f:id:sun-basix:20121009212050p:plain
chrome://chrome/extensions/ からデベロッパーモードを有効にして、
git cloneしたディレクトリをパッケージ化のところで指定すれば、
ビルドしてくれますので、あとはドラッグ&ドロップでインストールすれば使えます。

node.jsでtwitterのstatuses/update_with_mediaに対応

var Twitter = require('twitter')
  , request = require('request')
  , fs = require('fs');

Twitter.prototype.updateStatusWithMedia = function(text, params, callback) {
    var self = this
      , oauth = this.oauth
      , form, orderedParameters, r
      , url = 'http://upload.twitter.com/1/statuses/update_with_media.json';
    if (typeof params === 'function') {
        callback = params;
        params = {};
    }

    orderedParameters= oauth._prepareParameters(
        self.options.access_token_key,
        self.options.access_token_secret,
        'POST',
        url
    );
   
    r = request.post(url, callback)
    r.setHeaders({authorization: oauth._buildAuthorizationHeaders(orderedParameters)});
    form = r.form()

     form.append('status', text);
     for (i in params) {
         if (i == 'media[]') {
             form.append(i, fs.createReadStream(params[i]))
         }
         else {
             form.append(i, params[i]);
         }
     }

    return this;
};

var msg = 'API test',
    opt = {'media[]': __dirname+'/path/to/image'};

twit = new Twitter({
    consumer_key        : 'CONSUMER_KEY',
    consumer_secret     : 'CONSUMER_SECRET',
    access_token_key    : 'ACCESS_TOKEN_KEY',
    access_token_secret : 'ACCESS_TOKEN_SECRET',
});
twit.updateStatusWithMedia(msg, opt, function(err, res) {
    if (err) {
        console.log("twitter post error:\n", err);
    }
    else {
        res.setEncoding('utf8');
        var data = JSON.parse(res.body);
        console.log("twitter post success:\n", data);
    }
});

oauthライブラリの_performSecureRequestメソッドでやっているauthorizationヘッダの追加処理を入れて、form-dataライブラリを使ってアップすればOKだった。これなら、boundaryとかmime_typeを一切見る必要なく処理が書ける。

express.jsにgettext系の_関数を仕込む

引き続きnode.js系tips。
node-localizeというモジュールでの言語の管理がちょっとお手軽だったので、これとexpressを組み合わせるようにしてみた。

// express-localize.js
var Localize = require('localize');

module.exports = function(path, defaultLocale) {
    var locale, defaultLocale = defaultLocale || 'en';
    locale = new Localize(path, {}, defaultLocale);

    function _() {
        return locale.translate.apply(locale, arguments);
    }

    return function(req, res, next) {
        var render = res.render
          , lang   = req.query.lang || req.body.lang || req.session._lang || defaultLocale;
        req.session._lang = lang;
        locale.setLocale(lang);

        res.render = newRender;

        next();

        function newRender(template, stash) {
            stash._ = _;
            render.call(res, template, stash);
        }
    };
};

あとはapp.js内で

var expressLocalize = require('./lib/express-localize');
app.configure(function(){
  app.use(expressLocalize(__dirname + '/locale', 'ja'));
});

すれば使えるようになる。

node.jsでのクイックハック

jquery-deferredを使ったなんちゃってトランザクション
まだnode-mysqlでの使用しか想定してないので。

var $ = require('jquery-deferred');

function transaction(conn, callback) {
    var d = $.Deferred();
    d.done(function() {
        conn.query('COMMIT');
    }).fail(function() {
        conn.query('ROLLBACK');
    });
    conn.query('START TRANSACTION', function() {
        callback.apply({
            rollback: function() { d.reject.apply(d, arguments);  },
            commit  : function() { d.resolve.apply(d, arguments); }
        });
    });      
    return d.promise();
}

exports.transaction = transaction;

こんな感じでコードを書く

util.transaction(conn, function() {
    try {
        conn.query(SOME_SQL, function(err, results) {
            if (err) throw new Error;
        });
        this.commit();
    }
    catch(e) {
        this.rollback();
    }
}).done(function() {
    console.log("transaction success");
}).fail(function() {
    console.log("transaction fail");
});