2013年12月22日日曜日

Node.js向けORM Bookshelfの紹介

Node.js向けRDBを対象としたORM、Bookshelfを紹介します。

以下、Bookshelfの公式ページより:

BookshelfはNode.jsのためのプロミスベースのORMです、Knexというクエリービルダーを元に実装されています。Backbone.jsのモデルとコレクションを継承する形で実装しており、トランザクション、eager/nested-eagerリレーションのローディング、polymoriphic associations、1対1、1対多、多対多のリレーションをサポートしています。
PostgreSQL、MySQL、SQLite3を対象としています。

Bookshelf.js、Knex共にTim Griesser氏によるものです。Bookshelfを利用しているプロジェクトとしては最近話題の(?)Gohstがあります。

使ってみる


実際に使ってみるのがBookshelfの感覚をつかむのに近道と思います、簡便のためSQLite3を利用します。

インストールは以下の通り:

$ npm install knex bookshelf sqlite3 lodash

以下に今回のサンプルコード一式をおいて有ります、

$ git clone git clone https://p_baleine@bitbucket.org/p_baleine/bookshelf-sample.git

テーブルの作成


まずはKnexのマイグレーション機能を利用してテーブルを作成します。KnexのマイグレーションはRailsのそれの機能を極小に絞った簡易版です。以下の設定用モジュール(db/config.js)を作成し:

module.exports = {
  directory: './db/migrations',
  database: {
    client: 'sqlite3',
    connection: {
      filename: './db/sample.sqlite3'
    }
  }
};

以下コマンドを実行して、テーブル作成用のマイグレーションを生成します:

$ ./node_modules/.bin/knex migrate:make create_table -c db/config.js

<日付>_create_table.jsというファイルがdb/migrations配下に生成されます、これを編集します:

exports.up = function(knex, Promise) {
  return Promise.all([
    // postsテーブル
    knex.schema.createTable('posts', function(t) {
      t.increments('id').primary();
      t.string('title').notNullable();
      t.string('content').notNullable();
      t.timestamps();
    }),
    // commentsテーブル
    knex.schema.createTable('comments', function(t) {
      t.increments('id').primary();
      t.string('commenter').notNullable();
      t.string('content').notNullable();
      t.timestamps();
      t.integer('post_id').notNull().references('id').inTable('posts');
    })
  ]);
};

exports.down = function(knex, Promise) {
  return Promise.all([
    knex.schema.createTable('posts'),
    knex.schema.createTable('comments')    
  ]);
};

以下コマンドでマイグレーションを実行します:
  $ ./node_modules/.bin/knex migrate:latest -c db/config.js

これでdb/sample.sqlite3が生成されます、試しにテーブルが作成されているか除いてみます:

$ sqlite3 db/sample.sqlite3
  sqlite> .schema
  CREATE TABLE "comments" ("id" integer primary key autoincrement not null, "commenter" varchar(255) not null, "content" varchar(255) not null, "created_at" datetime, "updated_at" datetime, "post_id" integer not null, foreign key("post_id") references "posts"("id"));
  CREATE TABLE "knex_migrations" ("id" integer primary key autoincrement not null, "name" varchar(255), "batch" integer, "migration_time" datetime);
  CREATE TABLE "posts" ("id" integer primary key autoincrement not null, "title" varchar(255) not null, "content" varchar(255) not null, "created_at" datetime, "updated_at" datetime);

最初のモデル


投稿のモデルのコード(post.js)は以下の通りです:

var Bookshelf = require('bookshelf'),
    databaseConfig = require('./db/config').database,

    sampleBookshelf = Bookshelf.sampleBookshelf = Bookshelf.initialize(databaseConfig);

var Post = exports.Post = sampleBookshelf.Model.extend({
  tableName: 'posts',
  hasTimestamps: true
});

var Posts = exports.Posts = sampleBookshelf.Collection.extend({
  model: Post
});

`Bookshelf.initialize`でコネクションを取得しています、コネクションの取得タイミングについては後述します。モデルとしてPost、コレクションとしてPostsをexportします。

REPLから新しく投稿を保存してみます:
> var Post = require('./post').Post
> new Post({ title: 'Sample blog', content: 'sample content' }).save().then(function(post) {
.....   console.log(post.toJSON());
.....   })
> { title: 'Sample blog',
  content: 'sample content',
  updated_at: Sun Dec 22 2013 09:39:17 GMT+0900 (JST),
  created_at: Sun Dec 22 2013 09:39:17 GMT+0900 (JST),
  id: 1 }

コレクション経由で一覧を取得してみます:
> var Posts = require('./post').Posts
> Posts.forge().fetch().then(function(posts) {
...   console.log(posts.toJSON());
... });
> [ { id: 1,
    title: 'Sample blog',
    content: 'sample content',
    created_at: 1387672757272,
    updated_at: 1387672757272 } ]

リレーション


コネクションの取り扱い


投稿モデルに紐付くコメントモデルを作成してみます。2つ目のモデルの登場でコネクションの取得タイミングが問題になります。Bookshelf.initializeはアプリケーションのどこかのタイミングで一度だけ実行する必要があります。自分はアプリケーション中のモデルの基底クラスとなるBaseModelを作成して、このモジュール内でBookshelf.initializeを実行するようにして対応することが多いです(このやり方はGohstに習いました)。BaseModelのコード(base.js)は以下の通り:

var Bookshelf = require('bookshelf'),
    databaseConfig = require('./db/config').database,

    sampleBookshelf = Bookshelf.sampleBookshelf = Bookshelf.initialize(databaseConfig);

var BaseModel = exports.BaseModel = sampleBookshelf.Model.extend({
  hasTimestamps: true
}, {
  findAll: function() {
    var coll = sampleBookshelf.Collection.forge([], { model: this });
    return coll.fetch.apply(coll, arguments);
  }
});

ついでにfindAllメソッドを定義しておきます、正直モデルとコレクションを別個に保持するのは面倒なので、このfindAllは結構重宝します(これもGohst譲り)。 投稿モデルはBaseModelを継承するように変更します(post.js):

var BaseModel = require('./base');

var Post = pmodule.exports = BaseModel.extend({
  tableName: 'posts'
});

コメントモデル


コメントモデルのコードは以下の通り(commen.js):

var BaseModel = require('./base');

var Comment = module.exports = BaseModel.extend({
  tableName: 'comments'
});

また、投稿モデルからコメントモデルを参照するためにpost.jsのインスタンスメソッドに以下を追加します。

  comments: function() {
    return this.hasMany(require('./comment'));
  }

REPLで試してみます、id 1の投稿に対しコメントを作成してみます:

> var Comment = require('./comment');
undefined
> var comment = new Comment({ post_id: 1, commenter: 'p.baleine', content: 'sample comment' })
undefined
> comment.save().then(function(comment) { console.log(comment.toJSON()); })
[object Object]
> { post_id: 1,
  commenter: 'p.baleine',
  content: 'sample comment',
  updated_at: Sun Dec 22 2013 09:58:57 GMT+0900 (JST),
  created_at: Sun Dec 22 2013 09:58:57 GMT+0900 (JST),
  id: 1 }

投稿の一覧をコメント付きで取得してみます:

> Post.findAll({ withRelated: ['comments'] }).then(function(posts) {
... console.log(posts.toJSON());
... })
[object Object]
> [ { id: 1,
    title: 'Sample blog',
    content: 'sample content',
    created_at: 1387672757272,
    updated_at: 1387672757272,
    comments: [ [Object] ] },
  { id: 2,
    title: 'Sample blog',
    content: 'sample content',
    created_at: 1387673612433,
    updated_at: 1387673612433,
    comments: [] } ]

…肝心のコメントが省略されて見えない…、じゃぁこの投稿モデルだけ取得してみます:

new Post({ id: 1 }).fetch({ withRelated: ['comments'] }).then(function(post) {
... console.log(post.toJSON());
... })
> { id: 1,
  title: 'Sample blog',
  content: 'sample content',
  created_at: 1387672757272,
  updated_at: 1387672757272,
  comments: 
   [ { id: 1,
       commenter: 'p.baleine',
       content: 'sample comment',
       created_at: 1387673937358,
       updated_at: 1387673937358,
       post_id: 1 } ] }

トランザクション


最後に(自分がよくつまずくので)トランザクションについてです。サンプルとしては更にタグテーブルを追加して、投稿を保存する際にタグも追加するようにしてみます。

まずタグテーブル作成のマイグレーション生成:

$ ./node_modules/.bin/knex migrate:make create_tag -c db/config.js

生成されたマイグレーションを編集します:

exports.up = function(knex, Promise) {
  return Promise.all([
    knex.schema.createTable('tags', function(t) {
      t.increments().primary();
      t.string('text').notNullable();
      t.timestamps();
    }),

    knex.schema.createTable('posts_tags', function(t) {
      t.increments().primary();
      t.integer('post_id').notNull().references('id').inTable('posts');
      t.integer('tag_id').notNull().references('id').inTable('tags');
      t.unique(['post_id', 'tag_id'], 'post_tag_index');
    })  
  ]);
};

exports.down = function(knex, Promise) {
  return Promise.all([
    knex.schema.dropTable('tags'),
    knex.schema.dropTable('posts_tags')
  ]);  
};

マイグレーションを実行します:

$ ./node_modules/.bin/knex migrate:latest -c db/config.js

タグモデル(tag.js):

var BaseModel = require('./base');

var Tag = module.exports = BaseModel.extend({
  tableName: 'tags'
});

投稿モデル(post.js)を編集します、タグモデルに対し多対多のリレーションを定義して、`save`メソッドを上書きしています。投稿のsaveして、一度この投稿に紐付くタグを削除した上で、引数にもらったタグを保存する一連の処理をトランザクションでくるんでいます。

var _ = require('lodash'),
    BaseModel = require('./base'),
    Bookshelf = require('bookshelf').sampleBookshelf,
    Comment = require('./comment'),
    Tag = require('./tag');

var Post = module.exports = BaseModel.extend({
  tableName: 'posts',
  comments: function() {
    return this.hasMany(Comment);
  },
  tags: function() {
    return this.belongsToMany(Tag);
  },
  // override
  save: function(params, options) {
    var _this = this;

    return Bookshelf.transaction(function(t) {
      var tags = _this.get('tags');

      _this.attributes = _this.omit('tags');
      options = _.extend(options || {}, { transacting: t });

      return BaseModel.prototype.save.call(_this, params, options)
        .then(function() {
          // この投稿に紐付くタグを一旦全部削除
          return _this.tags().detach(null, options);
        })
        .then(function() {
          // 引数にもらったタグを追加
          return _this.tags().attach(tags, options);
        })
        .then(t.commit, t.rollback);
    }).yield(this);
  }
});

Bookshelf.transactionメソッドでトランザクションを実現します。コールバック関数がトランザクションのインスタンスを受け付けるので、ひき続く各処理saveとかattachとかにこれを引き回す必要があります。自分はよくこれを忘れてつまずいています。

REPLで試してみます、まずタグを作成(面倒くさいからknex経由):

var Post = require('./post') // base.jsを読み込む
var knex = Bookshelf = require('bookshelf').sampleBookshelf.knex
knex('tags').insert([{ text: 'sample1' }, { text: 'sample2' }]).then(console.log);

新しく投稿を作成、タグ`sample1`をつけてみます:

> var post = new Post({ title: 'sample blog', content: 'content', tags: [1] })
> post.save().then(function(post) { console.log(post.toJSON()); }).catch(console.error)
[object Object]
> { title: 'sample blog',
  content: 'content',
  updated_at: Sun Dec 22 2013 10:25:02 GMT+0900 (JST),
  created_at: Sun Dec 22 2013 10:25:02 GMT+0900 (JST),
  id: 3 }

上手くできたかフェッチして除いてみます(さっきの出力の投稿IDを利用):

> new Post({ id: 3 }).fetch({ withRelated: ['tags'] }).then(function(post) {
... console.log(post.toJSON());
... })
[object Object]
> { id: 3,
  title: 'sample blog',
  content: 'content',
  created_at: 1387675502759,
  updated_at: 1387675502759,
  tags: 
   [ { id: 1,
       text: 'sample1',
       created_at: null,
       updated_at: null,
       _pivot_post_id: 3,
       _pivot_tag_id: 1 } ] }

最後に


Bookshelfはプロミスベースなため、例えばExpress.jsで利用してみるとコントローラのコードの見通しが非常に良くなります。またBackboneのモデルを継承して実装されているため、バリデーションのロジック等クライアントサイドとサーバサイドのコードの共有が至極自然に行えます(この場合自分はよくbrowserifyを用います)。あと、これは宣伝ですが、やはりgruntインタフェースでマイグレーションを実行したくてプラグインを作って見ました grunt-knex-migrate。よかったら使ってみてください。

2013年5月3日金曜日

bowerとcomponent

以前component + Backbone.jsでTODOアプリ書きましたが、
今回はcomponentのライバル(?)bowerを試したくて
bower + RequireJS + Backbone.jsでTODOアプリを書いてみました。

たぶんここ1年、クライアントサイドのMVCにはまって書く30個目くらいのTODOアプリ、
いい加減サーバクライアントで実装するのに飽きたので今回はChromeのExtensionとして実現してみました。一応Chrome Storeにあげてあります
https://github.com/p-baleine/todo-extension
以下、componentとbowerを比較した感想と雑記です

componentとbower

componentExpressとかstylusで知られるTJが去年(2012)の8月くらいに リリースした、パッケージ管理 + モジュール化機構 + αなフレームワーク(ツール?)です。
  • Webアプリは外部(Github)及びローカルcomponentをボトムアップに組み合わせた、componentとして構築する
  • componentは必要なJavaScript、css、image、fontをひっくるめて提供する
  • 実はjQueryだとかのプラグインとして提供しているって事実は隠蔽して純粋なJavaScriptのオブジェクトとして提供する
…等、TJの哲学が色濃く出ていてるフレームワークです、詳細は起点のこのブログにて

http://tjholowaychuk.com/post/27984551477/components

bowerはTwitter主導で作っているパッケージ管理ツールで去年(2012)の9月くらいにリリースされています。

bowerは単なるパッケージ管理ツールなので、今回のTODOではモジュール管理にRequireJSを利用しています。

componentとbower両方でアプリを組んでみた実感としては、
両者はあまり比較対象としてふさわしくないなと思っています。
たしかにbowerは若干componentを参考にしているふしが見られるが
そもそも両者のスコープが違いすぎるし、componentの厳格な哲学に対して
bowerは節操がなさすぎ、
ネット上に転がってるbower.jsonもない単なるJSファイルをbowerのパッケージとして設定できる(https://github.com/p-baleine/todo-extension/blob/master/bower.json#L14)のはいかがなもんかと思った。
bowerとcomponentの比較は以下の記事が詳しいです

http://dailyjs.com/2013/01/28/components/

あと、この前のbower0.9へのアップデートでbowerパッケージの設定ファイルが
`component.json`から`bower.json`にリネームされましたが
これはTJがbowerに投げたこのissueが発端です

https://github.com/twitter/bower/issues/39

componentsというgithub上のグループがあります、
これはbowerのメンツがメインでcomponentとは関係ないようです(ややこしい)
componentsの目標はcomponent、bower、volo等々の
種々のパッケージ管理ツールに対応したComponentを提供すること、
実際にcomponents/jqueryとかみるとcomponent.json、bower.json
composer.jsonのファイルがおいて有ります。

まあ流行った方が残るんだろうが、個人的にはbowerはちょっと依存関係を解決してくれるダウンロードツールくらい、componentはRails並の強い哲学に基づいたフレームワークってイメージです。

Backobone 1.0.x

こっからは本当に雑記です。 今回初めてBackbone 1.0.xを使ってみました。 `Collection#fetch`の`reset: true`オプションが本当にオプションになったこと 以外はあまり意識しなかったかな、`View#listenTo`がどう作用するのか気になります。

Grunt 0.4.x

これも今回からです。 0.3.xではgruntでこける度に、さてgruntのソースから探るか それともgruntが内部で利用しているパッケージのソースを探るかって悩んだが、 0.4.x以降そもそも全て外部プラグインになっているので整然としてやりやすくなった気がします。

CoffeeScript

1年ぶりくらいにさわりました。 component含めExpressとかTJのプロダクトを利用しているとCoffeeScriptを 使う機会が減りますが(TJはcoffee嫌い https://github.com/visionmedia/jade/issues/430) 今回はそこから解放されてCoffeeScript使ってみました笑。 gruntでSourceMap enableでwatchしておくと そもそもそれがJavaScriptにコンパイルされている事実を意識せずにコーディングできるようになっていて結構感動でした。 Backbone.jsとの相性の良さも魅力、
;(function() {
  // ...
  var ListView = Backbone.View.extend({
    // ...
  });
}());
って書く代わりに
class ListView extends Backbone.View
  # ...
って書けるのは気持ち良いです。 `=>`のおかげで拙作`expect-change`もやっとRSpecっぽくなりました。
it "should remove `editable` class from el", ->
  expect(=> @view.$(".edit").trigger @event)
    .to.change(=> @view.$el.hasClass("editable")).from(on).to(off)

karma

いつの間にかtesacularからkarmaに名前変わっていたんですね。 以前はなぜかEmacsの吐くlockファイルが邪魔してテストの自動実行が出来なかったんですが、今回は特に問題もなく、   エディタでソース編集して保存→
    CoffeeScriptからJavaScriptへのコンパイル→
    karmaでテスト実行→
    テスト結果をGrowlで通知

まで自動化の理想的なフローを実現できました。
そもそもBDDではこれって理想というより必須だと思っています。
ただkarma + Require.jsはちょっと苦労しました。
同じ理由で躓く人がいたらこのリポジトリが参考になったらいいな

https://github.com/p-baleine/todo-extension/blob/master/app/coffee/spec-main.coffee

設定ファイル

これ、ルートディレクトリの直下のtreeです $ tree -L 1 ├── .bowerrc # bowerの設定ファイル ├── .git ├── .gitignore ├── .travis.yml # Travis CIの設定ファイル ├── Gruntfile.coffee # gruntの設定ファイル ├── README.md ├── app ├── bower.json # bowerパッケージの定義ファイル ├── icon.png ├── karma.conf.js # karmaの設定ファイル ├── manifest.json # Chrome Extensionのmanifest ├── package.json # nodeのパッケージの定義ファイル ├── screenshot.png ├── style.css └── todo.html まあjQueryのリポジトリとかみても似たようなものですが、 設定ファイル多すぎです。サーバサイドに比べて自分で選んだ フレームワークやツールの集まりでリポジトリが構成されていくので こうなっちゃうんだと思うんですが、 フロントエンドの人たちって職人臭強いなあと思います。

2012年12月18日火曜日

Backbone、Collection.createのエラーハンドラ


Backboneのドキュメントを見るとCollection.createについて

コレクションに新しいモデルのインスタンスを作成するときの便利メソッド。プロパティのハッシュを用いてモデルのインスタンスを作成し、モデルをサーバに保存して、これに成功した後コレクションにモデルを追加するのと等価。返却値はモデル、但しバリデーションエラー時はfalseが返却されモデルは作成されない。…

と書いて有ります。
これのバリデーションエラーのハンドラの登録の仕方についてです。
結論から言うと、イベントに対するハンドラの登録ではなく、
createメソッドのオプションでerrorハンドラを登録すべしです。

以下だらだらとその理由について、

通常Backboneにおいて、いわゆるハンドラはイベントの形で登録するのが一般的かと思います、
// collectionの`reset`イベントのハンドラにrenderListメソッドを登録
this.collection.on('reset', this.renderList, this);
Collection.createでModel.validateが`undefined`以外を返却した場合
(バリデーションエラーの場合)も以下のように
`error`イベントを捕まえられるかと思いますが
this.collection.on('error', this.alertError, this);
実際にCollection.createメソッドでModel.validateは`undefined`以外を返却しているが、
`error`イベントは発火されない、
答えはこのissueにありました。

Collection.createが失敗した時コレクションにはまだそのモデルは追加されていないのだから、
コレクションがまだ持っていないモデルのエラーを通知するのはおかしいということです。

DocumentCloudの人にそう言われると納得します、
なんでもかんでもイベントではなくて、`error`オプションの方が適切な文脈も
在るということですね。

2012年9月30日日曜日

[{ key: 3 }, { key: 2 }, { key: 1 }].should be_sorted_by(:key, :desc)

[{ key: 3 }, { key: 2 }, { key: 1 }].should be_sorted_by(:key, :desc) 
って記述したくて書いてみました


…一般的な用途だし、ちゃんと探せばライブラリとかあるのかな?

2012年6月20日水曜日

mongoDB、MapReduceでカレンダー


コレクションを、指定された期間で日付をキーにMapReduceした帳票を出力する、
って割とありふれたニーズだと思います。例えばこんなコレクションがあって、

<TODO>
contentdoneinsert
hoge2012/06/192012/06/17
piyo2012/06/19
foo2012/06/212012/06/19

以下のようなある週の日毎のTodo消化数一覧を表示したいとか。

  {"_id"=>2012-06-16 15:00:00 UTC, "value"=>{"done_count"=>0}}
  {"_id"=>2012-06-17 15:00:00 UTC, "value"=>{"done_count"=>0}}
  {"_id"=>2012-06-18 15:00:00 UTC, "value"=>{"done_count"=>1.0}}
  {"_id"=>2012-06-19 15:00:00 UTC, "value"=>{"done_count"=>0}}
  {"_id"=>2012-06-20 15:00:00 UTC, "value"=>{"done_count"=>1.0}}
  {"_id"=>2012-06-21 15:00:00 UTC, "value"=>{"done_count"=>0}}
  {"_id"=>2012-06-22 15:00:00 UTC, "value"=>{"done_count"=>0}}

PostgreSQLならgenerate_seriesで日付のテーブルを作ってこれに結合すれば期待する結果が得られると思います、mongoでも同様のことがしたくて試行錯誤してみました、
rubyでの実装です、

至極単純で、map関数でemitされるのと同様の形式でカレンダーコレクションを前もって作っておいて、これにMapReduceの結果をmergeするだけです。
カレンダーコレクション―MapReduceの出力先はランダム文字列と共に生成、使用後はdropして、衝突しないようにしています。
* moment.jsインクルードしていますが、実はこのpullリクエスト通らないと動きません、
 

結局これって、一時的なコレクション作ってそれにjoinしてるわけで、カレンダーに限らずSQLのwith句駆使して実装してる処理とかも(泥臭く、且つ分かり易く)実装できそう。

2012年5月10日木曜日

git pullのhookをglobalに設定する


git pullした際にanythingのファイルリストを更新したかったのですが、
やり方分からなかったので調べました。

まずgit pullの際のhook、
post-mergeに記述すれば良いみたいです。

それからhookのglobalな設定、
git initした際のテンプレート用のディレクトリをglobalに設定すれば良いみたいです。

なので、
$ # git initの際のテンプレートディレクトリ
$ mkdir -p ~/.git_template/hooks
$ # pullした時に実行したい処理を記述
$ vi ~/.git_template/hooks/post-merge
$ chmod a+x ~/.git_template/hooks/post-merge
$ # ~/.git_templateをglobalに設定
$ git config --global init.templatedir '~/.git_template'
$ # 既存のgitにテンプレートを追加
$ cd <既存のgit>
$ git init

新規作成やクローンしたgitには最初から~/.git_template/hooksの中身が
適用されています。既存のgitにgit initして問題ないのか不安だったので調べてみたら、
既存のリポジトリにgit initを実行しても問題ないです。既存のファイルが上書きされるだけです。git initを実行する主な目的は新規に追加されたテンプレートを適用するためです。
関係ないですがanythingのファイルリストの更新については当然
rubikitchさんの記事を参考にしてます。


2011年10月30日日曜日

FacebookのQuestionを取得

Geaph APIでQuestionが取得できるようになったそうです。 https://developers.facebook.com/docs/reference/api/question/ 
自分があまりQuestionを利用しないのでこのAPIの需要が分かっていないんですが…

  • Questionsの取得には「user_questions」又は「friends_questions」のpermissionが必要
  • /USER_ID/questionsでUSER_IDのQuestion一覧を取得
  • /QUESTION_ID/optionsで選擇肢を取得(この情報は/USER_ID/questionsに含まれているけど。。。) 
  • /QUESTION_OPTION_ID/votesで投票したユーザの一覧を取得 


試してみました、
require 'haml'
require 'koala'
require 'omniauth'
require 'rubygems'
require 'sinatra'

APP_ID = "212005055538084"
APP_SECRET = "hogehoge"

enable :sessions

use OmniAuth::Strategies::Facebook, APP_ID, APP_SECRET, :scope => 'user_questions' 

get '/auth/facebook/callback' do
  auth_hash = request.env["omniauth.auth"]
  uid = auth_hash["uid"]
  token = auth_hash["credentials"]["token"]
  graph = Koala::Facebook::API.new token

  result = graph.batch do |api|
    # 毎回:batch_args => ...て書くの気持ち悪いので特異メソッド定義
    def api.get_connects(id, conn, name="")
      self.get_connections(
        id, conn, {}, 
        :batch_args => { :name => name, :omit_response_on_success => false})
    end

    api.get_connects uid, "questions", "get-qs"
    api.get_connects "{result=get-qs:$.data[0].id}", "options", "get-ops"
    api.get_connects "{result=get-ops:$.data[0].id}", "votes"
    api.get_connects "{result=get-ops:$.data[1].id}", "votes"
    api.get_connects "{result=get-ops:$.data[2].id}", "votes"
  end

  haml '%div= result.to_s', :locals => { :result => result }

end
こんなjson返ってきました。
[
  [
    {
      "id": "170514669705292",
      "from": {
        "name": "Sinjin Ra",
        "id": "100002402797320"
      },
      "question": "今年の流行語は?",
      "created_time": "2011-10-30T12:25:51+0000",
      "updated_time": "2011-10-30T12:25:51+0000",
      "options": {
        "data": [
          {
            "id": "267486769961495",
            "from": {
              "name": "Sinjin Ra",
              "id": "100002402797320"
            },
            "name": "トゥットゥルー",
            "votes": 3,
            "created_time": "2011-10-30T12:25:50+0000"
          },
          {
            "id": "234349516624548",
            "from": {
              "name": "Sinjin Ra",
              "id": "100002402797320"
            },
            "name": "ドラマチックこそ、 人生です。",
            "votes": 2,
            "created_time": "2011-10-30T12:25:48+0000"
          },
          {
            "id": "309092772438842",
            "from": {
              "name": "Sinjin Ra",
              "id": "100002402797320"
            },
            "name": "ほびろん",
            "votes": 2,
            "created_time": "2011-10-30T12:25:49+0000"
          }
        ],
        "paging": { ...
..]

Herokuで試してみました、初Herokuです、
デプロイ(git push)したらアプリが動いているって画期的です、
…久々にClojure触りたくなってきました(HerokuはClojureもサポート)

2011年10月23日日曜日

OS XにGaucheをインストール


Shibuya.lispテクニカルトーク第7回行ってきました、
Shibuya.lisp、初参加です、

なにはともあれインパクト強すぎました、
Lispマシンを作ってみた(Youtube動画)
結局Lisp系って、「ないから」ではなく「あるけど」
「作りたいから作った」人が多いですね、

なので、久しぶりにScheme処理系作りたいと思いました。
事始めにGaucheのソースを追う環境を構築したのでその備忘録:

■Gaucheのインストール
・SLIB
$ cd ~/Downloads
$ curl -O http://groups.csail.mit.edu/mac/ftpdir/scm/slib-3b3.zip
$ unzip slib-3b3.zip 
$ cp -r slib ~/opt/slib
・Gauche
ダウンロード後、
$ tar zxvf Gauche-0.9.2.tgz
$ cd Gauche-0.9.2/
$ ./configure --prefix=/Users/tajima_junpei/opt/Gauche-0.9.2 --with-slib=/Users/tajima_junpei/opt/slib
$ make
$ make install
$ echo "export PATH=\$PATH:/Users/tajima_junpei/opt/Gauche-0.9.2/bin" >> ~/.bashrc
$ source ~/.bashrc
・確認
$ gosh
gosh> (define (fact n) (if (> n 1) (* n (fact (- n 1))) 1))
fact
gosh> (fact 10)
3628800
gosh> (use slib)
#
gosh> (require 'trace)
#t
gosh> (trace fact)
#
gosh> (fact 5)
CALL fact 5
  CALL fact 4
    CALL fact 3
      CALL fact 2
        CALL fact 1
        RETN fact 1
      RETN fact 2
    RETN fact 6
  RETN fact 24
RETN fact 120
120
gosh> 

■ソースを追うための準備
・GLOBALのインストール
$ cd ~/Downloads
$ curl -O http://tamacom.com/global/global-6.1.tar.gz
$ tar zxvf global-6.1.tar.gz
$ cd global-6.1/
$ ./configure --prefix=/Users/tajima_junpei/opt/global-6.1
$ make
$ make install
~/.emacs.d/.init.elに追記
;; gtags
(autoload 'gtags-mode "gtags" "" t)
(setq gtags-mode-hook
      '(lambda ()
         (local-set-key "\M-t" 'gtags-find-tag)
         (local-set-key "\M-r" 'gtags-find-rtag)
         (local-set-key "\M-s" 'gtags-find-symbol)
         (local-set-key "\C-t" 'gtags-pop-stack)
         ))

(autoload 'gtags-mode "gtags" "" t)
(setq gtags-mode-hook
      '(lambda ()
         (local-set-key "\M-t" 'gtags-find-tag)
         (local-set-key "\M-r" 'gtags-find-rtag)
         (local-set-key "\M-s" 'gtags-find-symbol)
         (local-set-key "\C-t" 'gtags-pop-stack)
         ))
ソースの関数上でM-tでタグジャンプ

■ソースの追い方
Emacs上にて
M-x gdb
gdb --annotate=3 gosh
(gdb) break repl_read_cc
Breakpoint 1 at 0x20c49ba5d77dd1: file repl.c, line 103.
(gdb) r
Starting program: /Users/tajima_junpei/opt/Gauche-0.9.2/bin/gosh 
Reading symbols for shared libraries ++. done
gosh> (+ 1 1)

関数repl_read_cc上にて実行が止まる、
「s」でステップ実行、「p」で変数のインスペクト

2011年9月9日金曜日

Google APIクライアントライブラリ

今日まで気付かなかったのですが、
最近のGoogle Codeのブログ、やたらとGoogle APIクライアントライブラリに関する投稿が多いです。

これらプロジェクトの関連を見つけられていないんですが、
何かしら大元のプロジェクトの存在が窺えるような…

Googleのライブラリって
「YouTubeであればPythonとPHPと…のライブラリが提供さえている」と、
プロダクトごとにカテゴライズされるイメージが強いですが、
最近のGoogle Codeブログの投稿を見ていると、
「各言語について、すべてのGoogleプロダクトのAPIを提供する(つもりの)ライブラリを準備中…」
と読めなくもないような

探してみたらRubyのライブラリもありました、お陰で週末の予定も埋まりました。


…Google三大言語なんて既に、Googler以外の人が信じる迷信なんじゃないだろうか?

* 2011/9/15 追記
ターゲットはGoogle+ APIだったんですね、なるほどー

2011年7月29日金曜日

もっと簡単にFacebookアプリのテストユーザを作成

* 2011/8/3時点で、以下記載のテストユーザ作成スキームがダウンしています。早ければ明日(2011/8/4)復旧予定とのことです。(http://developers.facebook.com/live_status/#msg_804)


2011/8/11、復活してます。


以前プログラムにてFacebookのテストユーザを作成する方法について投稿しましたが、現在開発者ページよりテストユーザの作成が可能となっています。

開発者ページより任意のアプリを選択、[Roles]の[編集]をクリックします。
[Test Users]の[追加]をクリックすることで新たにテストユーザを作成できます。

一度テストユーザを作成すると[Test Users]に追加される[Modify]メニューより以下の操作が可能です。
  • テストユーザの名前の変更
  • ・テストユーザどうしを友達にする
  • テストユーザを他のアプリに追加する
  • テストユーザを削除する
http://developers.facebook.com/blog/post/527/

2011年6月26日日曜日

さくらのVPS(Ubuntu)にredmineをインストール

redmineはruby 1.8を要しますが、
VPSとしてはruby 1.9がイイので、
privateにruby 1.8を利用するredmineユーザを作成して対応しました。
(普通、こういう状況ってどうやって対処するのでしょうか?)

rubyのインストール

$ sudo adduser redmine
$ sudo usermod -a -G admin redmine
$ su redmine
$ mkdir ~/opt
$ mkdir ~/src
$ cd ~/src
$ wget ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.7-p334.tar.bz2
$ tar jxvf ruby-1.8.7-p334.tar.bz2
$ cd ruby-1.8.7-p334
$ ./configure --prefix=$HOME/opt/ruby-1.8.7-p334
$ make
$ make install
$ ln -s ~/opt/ruby-1.8.7-p334 ~/opt/ruby
$ echo "export PATH=\$HOME/opt/ruby/bin:\$PATH" >> ~/.bashrc
$ source ~/.bashrc

rubygemsのインストール

$ cd ~/src
$ wget http://rubyforge.org/frs/download.php/74374/rubygems-1.6.0.tgz
$ tar zxvf rubygems-1.6.0.tgz
$ cd rubygems-1.6.0
$ ruby setup.rb
$ mkdir -p ~/opt/lib/ruby/gems/1.6
$ cat >> ~/.bashrc
export GEM_HOME=$HOME/opt/lib/ruby/gems/1.6
export PATH=$GEM_HOME/bin:$PATH
export RUBYLIB=$RUBYLIB:$GEM_HOME/lib
$ source ~/.bashrc

必要なgemファイルのインストール

$ gem install rails -v=2.3.11
$ gem install rack -v=1.0.1
$ gem install postgres-pr
$ gem install pg -- --with-pg-dir=/opt/postgresql
$ gem install -v=0.4.2 i18n

redmineのインストール

$ cd /opt
$ sudo svn checkout http://redmine.rubyforge.org/svn/branches/1.2-stable redmine
$ sudo chown -R redmine:redmine redmine/

DBの設定

$ sudo -u postgres /opt/postgresql/bin/createuser -s -P
ユーザ名を入力
パスワードを入力
$ /opt/postgresql/bin/createdb -U ユーザ名 パスワード

redmineの設定

$ cd /opt/redmine
$ cp config/database.yml.example config/database.yml
$ vi config/database.yml
production:
adapter: postgresql
database: データベース名
host: localhost
username: ユーザ名
password: パスワード
encoding: utf8
schema_search_path: public
$ rake config/initializers/session_store.rb
$ rake db:migrate RAILS_ENV="production"
$ rake redmine:load_default_data RAILS_ENV="production"
$ sudo ufw allow 3000/tcp
$ ruby script/server webrick -e production &

passengerの導入がうまく行かないー。。
…その内対応します

2011年5月15日日曜日

Mac OS XにRuby 1.9.2とRails 3.0.7をインストール

せっかくAgile Web Development with Rails第4版を予約してまで購入したので、Mac OS XにRuby 1.9.2とRails 3.0.7をインストールします。

Rubyのインストール先:$HOME/opt/ruby-1.9.2-p180
gemファイルの格納先:$HOME/opt/lib/ruby/gems/1.8

Rubyのインストール


$ cd ~/tmp
$ curl -O ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.2-p180.tar.bz2
$ tar jxvf ruby-1.9.2-p180.tar.bz2
$ cd ruby-1.9.2-p180
$ ./configure --prefix=$HOME/opt/ruby-1.9.2-p180
$ make
$ make install
$ ln -s ~/opt/ruby-1.9.2-p180 ~/opt/ruby
$ echo "export PATH=\$HOME/opt/ruby/bin:\$PATH" >> ~/.bashrc
$ source ~/.bashrc

rubygems-updateのインストール

$ mkdir -p ~/opt/lib/ruby/gems/1.8
$ cat >> ~/.bashrc
export GEM_HOME=$HOME/opt/lib/ruby/gems/1.8
export PATH=$HOME/opt/lib/ruby/gems/1.8/bin:$PATH
export RUBYLIB=$RUBYLIB:$HOME/opt/lib/ruby/gems/1.8/lib
$ source ~/.bashrc
$ gem install rubygems-update
$ update_rubygems
$ gem uninstall rubygems-update
あまりよく分かっていないです、update_rubygemsした時点でrubygems-updateに不整合が起きるらしいので、一度アンインストールしています、↓で再度インストールし直しています。

railsのインストール

$ gem install rubygems-update twitter rails sqlite3

確認

$ rails new test_app
$ cd test_app/
$ rails server
http://localhost:3000/にアクセスしてAbout your application’s environmentをクリック

2011年5月14日土曜日

php-sdkを用いてFacebookアプリのテストユーザを作成

* (2011/8/11追記) Webページよりテストユーザを作成するスキームが新たに公開されています。

php-sdkを用いてFacebookアプリのテストユーザを作成します。
環境はZend Frameworkを想定しています。

テストユーザの作成

以下パラメータを指定できます。
  • installed:テストユーザ作成時にテストユーザにアプリをインストールするか否かを指定。デフォルトはtrue
  • permissions:installedがtrueの場合、アプリに承認される権限を指定。

// テストユーザ作成
public function createTestUserAction() {
  $installed = $this->getRequest()->getParam('installed');
  $permissions = $this->getRequest()->getParam('permissions');
  $paramAry = array(
    'access_token' => $this->getAppAccessToken(),
    'installed' => empty($installed) ? 'true' : $installed,
    'permissions' => empty($permissions) ? 'read_stream,publish_stream' : $permissions
    );
  print_r($this->fb->api("/{$this->_fbAppId}/accounts/test-users", "POST", $paramAry));
}
例えば以下URLでread_stream、publish_stream、emailの権限が承認されたテストユーザを作成します。
/<コントローラ名>/create-test-user?permissions=read_stream,publish_stream,email
login_urlが出力されるので、このURLを用いてテストユーザにアクセスできます。
login_urlは1時間で無効になります。

テストユーザの表示

// テストユーザ一覧表示
public function viewTestUsersAction() {
  $tokenUrl = "/{$this->_fbAppId}/accounts/test-users?{$this->getAppAccessToken()}";
  print_r($this->fb->api($tokenUrl));
}
アプリに対し作成済みのテストユーザ一覧を表示します。
/<コントローラ名>/view-test-users
出力されるlogin_urlは10分で無効になります。

テストユーザの削除

// テストユーザ削除
public function deleteTestUserAction() {
  $userId = $this->getRequest()->getParam('user-id');
  $paramAry = array('access_token' => $this->getAppAccessToken());
  $this->fb->api("/{$userId}", "DELETE", $paramAry);
}

※アプリのaccess_token取得用関数
private function getAppAccessToken() {
  $tokenUrl = "https://graph.facebook.com/oauth/access_token?" .
    "client_id={$this->_fbAppId}&client_secret={$this->_fbSecret}" .
    // $this->_fbAppID:アプリID
    // $this->_fbSecret:アプリの秘訣
    "&grant_type=client_credentials";
  $matches = array();
  preg_match('/access_token=(.+)/', file_get_contents($tokenUrl), $matches);
  $accessToken = $matches[1];
  return $accessToken;
}

以下メモです。
  • テストユーザのlogin_urlは1時間で無効となるが、再取得は可能
  • テストユーザのaccess_tokenが取得可能のため、APIを用いてテストユーザに代わりアクションを取れる
  • 1アプリにつき500テストユーザまで作成可能
  • 現在はAPIを介したアクセスのみ可能だが、Developerアプリを介したアクセスもサポートされる予定