Rails のコンローラのフィルタのようなものを実装してみる

written by taka on December 31st, 2009 @ 07:42 PM  

Rails のコントローラで利用できるフィルターのような機能を Rails ではないアプリに組み込みたく、実装してみることにした。

ちなみに、Rails のコントローラ内で利用できるフィルタには、以下のものがある。

  • before_filter: アクションの前に実行
  • after_filter : アクションの後に実行
  • around_filter: アクションの前後で実行

あまりよい例を出せないのだが、現在以下のクラスが既に実装されているものとする。

1
2
3
4
5
6
7
8
9
class Greeting
  def initialize(out = STDOUT)
    @out = out
  end

  def hello
    @out.puts 'hello!'
  end
end

Rails のフィルターのように上記の hello メソッドの前後で実行するための機能をClass Macroで追加できるようにしたい。 実装イメージとしては、以下のような感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Greeting
  ...
  before_filter :before_greeting
  after_filter :after_greeting

  ...

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

Greeting.new.hello
# >> Ya!      # 「hello!」の前に表示される
# >> hello!
# >> bye!     # 「hello!」の後に表示される

通したいテストは以下のような感じ。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'test/unit'

class GreetingTest < Test::Unit::TestCase
  def setup
    @out = StringIO.new
    @greeting = Greeting.new(@out)
  end

  def test_running_filters
    @greeting.hello
    assert_equal ['Ya!', 'hello!', 'bye!'], @out.string.split("\n")
  end
end

やることは、2つ。

  • 既に実装されているGreeting#helloメソッドには手を入れないで機能を追加する。
  • 前後に呼び出す機能をコールバックとして定義し、実行できるようにする。

全て1から実装するのはシンドイので。。なにかと便利な ActiveSupport を使う。

ActiveSupport には、ActiveSupport::Callbacks というモジュールがあり、Rails のコントローラのフィルタでもこのモジュールが利用されている。拝借する。

まずは、前後に挟む機能を定義するためのフィルタの準備。モジュールで準備する。

1
2
3
4
5
6
7
8
9
10
11
12
require 'rubygems'
require 'active_support'

module Filters
  def self.included(base)
    base.class_eval do
      include ActiveSupport::Callbacks

      define_callbacks :before_filter, :after_filter
    end
  end
end

このモジュールは、利用するクラスでインクルードされると、

  • ActiveSupport::Callbacks モジュールをインクルードする。
  • ActiveSupport::Callbacks モジュールで定義されている define_callbacks メソッドで :before_filter:after_filter をコールバックメソッドとして定義する。

ということを行う。

これで、Greeting クラスで Filters モジュールをインクルードすれば、before_filterafter_filter でコールバックメソッドが定義できるようになる。

後は、Greeting クラスの中で、それぞれのコールバックされるメソッドを記述し、コールバックメソッドの呼び出しを行なわないといけない。

Open ClassGreeting クラスを再定義し、Around Alias で既にある hello メソッドに機能を追加する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Greeting
  include Filters

  before_filter :before_greeting
  after_filter :after_greeting

  def hello_with_filters
    run_callbacks(:before_filter)
    hello_without_filters
    run_callbacks(:after_filter)
  end

  alias_method :hello_without_filters, :hello
  alias_method :hello, :hello_with_filters

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

最終的に1つのファイルに以下のように記述している。

filter_example1.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 既存のクラス
class Greeting
  def initialize(out = STDOUT)
    @out = out
  end
  
  def hello
    @out.puts 'hello!'
  end
end

require 'rubygems'
require 'active_support'

module Filters
  def self.included(base)
    base.class_eval do
      include ActiveSupport::Callbacks

      define_callbacks :before_filter, :after_filter
    end
  end
end

# Greetin クラスを再定義
class Greeting
  include Filters

  before_filter :before_greeting
  after_filter :after_greeting

  def hello_with_filters
    run_callbacks(:before_filter)
    hello_without_filters
    run_callbacks(:after_filter)
  end

  # ActiveSupport::CoreExtensions::Module#alias_method_chain を使えば1行で書ける
  alias_method :hello_without_filters, :hello
  alias_method :hello, :hello_with_filters

  private
  def before_greeting
    @out.puts 'Ya!'
  end

  def after_greeting
    @out.puts 'bye!'
  end
end

# テスト
require 'test/unit'

class GreetingTest < Test::Unit::TestCase
  def setup
    @out = StringIO.new
    @greeting = Greeting.new(@out)
  end

  def test_running_filters
    @greeting.hello
    assert_equal ['Ya!', 'hello!', 'bye!'], @out.string.split("\n")
  end
end

テストを実行。

1
2
3
4
5
6
7
$ ruby filter_example1.rb
Loaded suite filter_example1
Started
.
Finished in 0.000389 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

OK だ。

このくらいのことであれば、AspectR を使うこともできるが、より自由な組み込みができそう。

bash コマンドをもっと便利に使う

written by taka on December 19th, 2009 @ 10:28 PM  

地味ではあるが、bash が標準でもっている機能だけで、知っておくと結構便利に bash コマンドを利用できるので、メモしておく。

ディレクトリの移動

ディレクトリをスタックして活用する/pushdpopddirs

ディレクトリの移動には、pushdpopddirs コマンドに慣れると便利。

pushd は移動したディレクトリをディレクトリスタックに追加していく。

popd はその逆で、ディレクトリスタックを削除していく。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ pwd
/Users/taka
$ pushd ~/Documents/
~/Documents ~
$ pwd
/Users/taka/Documents
$ pushd ~/Desktop/
~/Desktop ~/Documents ~
$ pwd
/Users/taka/Desktop
$ pushd ../work/radiant/tmp/
~/work/radiant/tmp ~/Desktop ~/Documents ~
$ pwd
/Users/taka/work/radiant/tmp

追加されたディレクトリは、dirs -v で確認できる。

1
2
3
4
5
$ dirs -v
 0  ~/work/radiant/tmp
 1  ~/Desktop
 2  ~/Documents
 3  ~

上記のディレクトリスタックの状態で、~/Documents に移動したい場合は、pushd +n を使う。n には、上記リストの左側に表示されている番号を使用する。

1
2
3
4
$ pushd +2
~/Documents ~ ~/work/radiant/tmp ~/Desktop
$ pwd
/Users/taka/Documents

ちょっとしたことだが、楽になる。

~/Desktop をディレクトリスタックから削除する場合には、

1
2
3
4
5
6
$ popd +1
~/work/radiant/tmp ~/Documents ~
$ dirs -v
 0  ~/work/radiant/tmp
 1  ~/Documents
 2  ~

直前のディレクトリに戻る/cd -

直前のディレクトリに戻りたい時は、cd- の引数を与えればよい。

1
2
3
4
5
6
$ cd ~/Documents/
$ cd ~/work/
$ cd -
/Users/taka/Documents
$ pwd
/Users/taka/Documents

引数の再利用

前回利用したコマンドを再度利用する場合などには、

  • 直前のコマンドであれば、!!
  • ヒストリからの再利用であれば、!nn は、history コマンドで表示される履歴の番号

などが便利だが、この考え方は、引数にも利用できる。

あるファイルをコピーして、そのファイルを vi などで開く場合、

1
2
$ cp a.txt b.txt
$ vi b.txt

これは、以下の方法でいける。

1
2
$ cp a.txt b.txt
$ vi !:2

! を使うのはコマンドの時と同じ。その後に:(コロン)と n(数字)と入力する。数字の部分が前回のコマンドで利用した引数の位置。 上記の場合は、b.txt を指定したいので、前回コマンドの2番目ということで、2 を指定している。

また、最後の引数を使いたい場合には、M-. (ESC キーを押した後に、.)を入力すると、最後に使用したコマンドの最後の引数を自動で入力してくれる。

上記のコマンドであれば、vi と入力して、スペース、M-. と入力すれば、vi b.txt と入力される。

{}(ブレース)を利用したファイルパスの展開

{}(ブレース)による文字列の展開は便利に使える。

1
2
$ echo {one,two,three}sheep
onesheep twosheep threesheep

{}(ブレース)の中で、文字列を並べる際にカンマの後にスペースを入れたくなるが、入れてしまうと展開してくれないので注意。

日常生活でどう使えるか?

ある設定ファイルを編集しようとする時に、その設定ファイルのバックアップファイルを作っておくのはよくやる行為。

{}(ブレース)を使って、以下のように書ける。


$ sudo cp -p /etc/apache2/httpd.conf{,.back}

ちょっと奇妙に見えるが、結果として、/etc/apache2 ディレクトリに httpd.conf ファイルのコピーである httpd.conf.back ファイルが作成される。

{}(ブレース)の最初のところで、{ の後に,(コンマ)だけというのがミソで、上記のコマンドは、以下のように展開される。


$ sudo cp -p /etc/apache2/httpd.conf /etc/apache2/httpd.conf.back

編集後、diff を取る時にも同じ要領で便利に使える。


$ diff -u /etc/apache2/httpd.conf{,.back}

履歴(history)を活用

履歴をインクリメンタル検索/C-r

上下矢印キー、もしくは、C-p(コントロールキーを押しながらp) or C-n でコマンドの履歴を表示できるが、探したいコマンドがある程度予測のついている場合には、C-r が便利。

C-r を入力後、過去に入力していたはずで、今回使用したいコマンドの文字列を入力すると、部分一致でインクリメンタルサーチとなる。入力一文字一文字で過去の履歴の中から検索してくれる。

history から検索しているので、コマンドそのものでなくても、引数として預けた文字列でも探してくれる。

希望のものが見つかったらそのまま Enter で即実行だが、引数などをちょっと変更したいときには、C-fなり、C-bなどをタイプし、C-r から抜けると、それまで部分一致で表示されていたコマンドがそのままプロプトに表示されるので、好きに編集する。

履歴の残し方をカスタマイズ

連続して入力した同じコマンドを全て履歴として残すことにはあまり必要性を感じないなぁという場合には、連続して入力された同じコマンドは残さないようにすることができる。


$ export HISTCONTROL=ignoreboth

コマンドが発行された時間を残しておきたい場合には、以下のようにする。

1
2
3
4
5
6
7
8
9
$ export HISTTIMEFORMAT='%Y-%m-%d %T '
$ history
...
  496  2009-12-19 21:56:37 history
  497  2009-12-19 21:56:40 cp -p a.txt c.txt
  498  2009-12-19 21:56:45 cp -p a.txt d.txt
  499  2009-12-19 21:56:49 history
  500  2009-12-19 21:58:50 exit
  501  2009-12-19 21:58:57 history

Ruby YAML ライブラリでの UTF-8 な日本語の扱い

written by taka on November 29th, 2009 @ 07:00 AM  

日本発の言語でありながら、Ruby の YAML 標準ライブラリには弱点がある。。

Ruby の YAML ライブラリは、日本語を正しく扱ってくれない。

具体的には、Object#to_yaml を使用した際に、UTF-8 な日本語が binary 扱いになってしまう。

1
2
3
4
5
6
7
8
9
$ irb --prompt simple
>> $KCODE
=> "NONE"
>> $KCODE = 'u'
=> "u"
>> str_ja = '日本語'
=> "日本語"
>> str_ja.to_yaml
=> "--- !binary |\n5pel5pys6Kqe\n\n"

この問題を回避するためのライブラリが幾つかある。

ここでは、yaml_waml を使ってみる。

yaml_waml を使う準備

gem で提供されているので、インストールしていない場合はインストールしておく。
なお、gemcutter に移行されているので、gemcutter をまだ使っていない場合は、先に準備しておく。

1
2
3
4
5
6
7
8
$ gem install gemcutter
$ gem tumble
$ gem sources
*** CURRENT SOURCES ***

http://gemcutter.org
http://gems.rubyforge.org/
http://gems.github.com

yaml_waml のインストール。

1
2
3
4
5
$ gem install yaml_waml
Successfully installed yaml_waml-0.3.0
1 gem installed
Installing ri documentation for yaml_waml-0.3.0...
Installing RDoc documentation for yaml_waml-0.3.0...

yaml_waml を使ってみる

先程の irb のプロンプトに引き続き実験。

1
2
3
4
>> require 'yaml_waml'
=> true
>> str_ja.to_yaml
=> "--- ""\n"

OK だ。

Rails で使用する

Rails で対応する場合には、config/environment.rb で gem の指定しておく。

1
2
3
4
5
Radiant::Initializer.run do |config|
...
  config.gem 'yaml_waml', :version => '~> 0.3', :source => 'http://gemcutter.org'
...
end

のような感じ。

ブラウザのテキスト入力を好みのエディタで

written by taka on November 25th, 2009 @ 08:38 PM  

"It's All Text!" を使ってみることに

最近になってこんな便利なものがあることに気づいた。。

Web アプリケーションはホントに便利になり、大半のことはブラウザで行えてしまう。

ただ・・・wiki、ブログ、tracking system などテキストを入力する機会が多いアプリを扱う際には、ブラウザのテキストボックスの入力欄ではちょっとストレスが溜まる。
最近はそれを補うワードプロセッサーのような機能を提供するものもあるが、好みのエディタで編集したいという欲望は抑え切れない。

と、Radiant CMS の screencast を見ていた時にテキストボックスの編集を TextMate で行っているものがあった。そういうツールが既にあるんだ!ということでちょっと調べてみた。

という Firefox のアドオンもあるようだ。
他にも、textserver というものがある。

好んで使用しているのは Mac OS なのだが、Windows も使う必要がある。両方でうまく動作したのが It's All Text! の方だったので、しばらくこちらを使っていくことにした。

ブラウザのテキストエリアを Emacs で編集

エディタは Emacs である必要はないのだが、大半のこと Emacs でやってしまっている身としては、Emacs が使えると非常にありがたい。
他エディタでも同様のことは可能だが、テキストボックスに入力するテキストの入力フォーマットに合わせて Emacs 側で編集モードをかえて編集できるのは便利だ。 Markdown であれば、Emacs markdown-mode、HTML であれば nXhtml - an Emacs mode for Web Development などという感じ。非常に快適だ。

インストールと設定

Firefox へのインストールは、下記から通常の手順で実施する。

アドオンの設定には、以下の設定を行う。

It's All Text Setting

上記は、Mac OS X の Carbon Emacs をエディタとして使用する場合の設定。上記のエディタのパスは汎用的なものではない。理由については後述する。

既に起動している Emacs のフレームのバッファを使用する

Carbon Emacs のパスは、/Applications/Emacs.app/Contents/MacOS/Emacs となるので、これを指定してあげればいいのだが、これだと "It's All Text!" を使う度に、新たにフレームから立ち上げてしまう。Emacs は常に立ち上っているので、編集の度にフレームが複数上がるのはメンドい。既に起動している Emacs のバッファに表示させるようする。

こういう時には、Emacs Server/Client を使えばよいようだ。

エディタのパスには、

  • /Applications/Emacs.app/Contents/MacOS/bin/emacsclient

を指定しておいて、Emacs 側では、Emacs Server を起動しておく。


M-x server-start

常に起動させるようにするには、.emacs なりに、


(server-start)

を記入しておく。

ただ、1つ問題が・・・
何故か設定画面にて、emacsclient のフルパスが保存されない。理由はよくわからないが。

仕方ないので、自身のホームディレクトリ配下の bin 配下に上記のシンボリックリンクを用意して、それを参照させる。

1
2
$ cd ~/bin
$ ln -s /Applications/Emacs.app/Contents/MacOS/bin/emacsclient emacsclient

これで、最初の設定画面のエディタのパスに /Users/taka/bin/emacsclient が入ることになった。

Emacs Client からバッファを開いた場合には、C-x # でそのバッファを終了させるのがお行儀のよいやり方。

使い方

テキストボックスにカーソルを持っていくと、デフォルトでは右下(設定で変更可能)に "編集" ボタンがでるので、それをクリック。設定したエディタがアクティブになる。

マウスでわざわざクリックしなくても、ショトカットキーも割り合てることもできる。こちらも設定画面にて設定。

It's All Text! Button

以下は、エディタ(Emacs)側で編集を行っている画面。エディタ(Emacs)で保存を行えば、それがブラウザのテキストボックスに反映される。

It's All Text! Edit

もっと早く知っておけばよかった。。

Twitter で Unfollowers (アンフォローをした方)をチェックする簡易スクリプト

written by taka on November 7th, 2009 @ 04:01 PM  

スクリプトを準備した経緯

Twitter を始めて早2ヶ月が経とうとしている。

「Google Reader なんかもういらない!」(Rest in Peace, RSS)という記事に触発されて始めてみたのだが、自分自身には相変わらず Google Reader は必需品だ。。(用途の棲み分けはできている)

ただ、これまで触れ合うことはなかった方々と触れ合えたり、Google Reader と合わせて貴重な情報源となっており、今では Twitter も必需品だ。

そんな中、「Twitter で Unfollowers (アンフォローをした方)を調べるにはどうしたらいいの?」という質問を受けた。

現状身近な方には特に知らせることも無く、こっそり静かに Twitter を使っているのだが、自身の follower さんも少しづつ増えてきている。
この follower さん、増えるだけでは無く、当然ながら減ることもある。フォローされた場合は、メール通知の設定をしておけば、メールで知ることができるが、確かに、一度フォローした後、そのフォローを解除している方を知るための機能は標準では無さげだ。(きっと)

標準では無さそうだが、同様のことをしてくれるサービスは既にあるだろう。車輪の再開発だが、Twitter のサードパティのサービス/アプリは多すぎて、、、簡単なものであれば自分で準備した方が手軽だ。

しかし、もしかすると、これからやろうとしていることはパンドラの箱なのかもしれない。。
例えば、妻のアカウントからフォローされていたのに、ある日突然そのフォローが外れていたら、どう対応すればよいのだろう・・・知らぬが仏のような気もする。。

Unfollowers をチェックするスクリプト

まずは、Twitter API とやり取りを行うベースとなる簡易 Twitter クライアントを用意する。適当な名前付けでSimpleTwitterとする。

SimpleTwitter

httparty gem がインストールされていない場合には、事前にインストール。

1
2
3
4
5
6
$ sudo gem install httparty
$ gem list httparty

*** LOCAL GEMS ***

httparty (0.4.3)

今回の目的からは外れるが、Timeline の表示、Mention の表示も一応入れておく。

simple_twitter.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
require 'time'
require 'forwardable'

require 'rubygems'
require 'httparty'

class SimpleTwitter
  extend Forwardable
  include HTTParty
  base_uri 'https://twitter.com' # SSL で

  def_delegator :"#{self}", :get, :get

  def initialize(user, password, options = {})
    if options.key? :http_proxy
      options[:http_proxy_port] ||= 8080
      self.class.http_proxy options[:http_proxy], options[:http_proxy_port].to_i
    end
    @auth_info = { :username => user, :password => password }
  end

  def timeline(which = :friends, options = {})
    auth_info_merge! options
    get("/statuses/#{which}_timeline.json", options)
  end

  def mentions(options = {})
    auth_info_merge! options
    get("/statuses/mentions.json", options)
  end

  def followers(options = {})
    auth_info_merge! options
    options[:query] ||= {}
    options[:query].merge!({ :cursor => -1 }) unless options[:query].key? :cursor
    get("/statuses/followers.json", options)
  end

  class << self
    # 日付表示のフォーマット
    def format_time(time, format = '%Y/%m/%d %H:%M:%S')
      (time.is_a?(String) ? Time.parse(time) : time).strftime(format)
    end
  end

  private
  def auth_info_merge!(options)
    options.merge!({ :basic_auth => @auth_info })
  end
end

タイムラインの表示などには、以下のような感じで使える。

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'simple_twitter'

# 'user', 'password' には Twitter のログインアカウントとパスワードを指定
twitter = SimpleTwitter.new('user', 'password')
# Proxy を経由する必要がある場合
# twitter = SimpleTwitter.new('user', 'password',
#   { :http_proxy => 'proxy_server', :http_proxy_port => 8080 })

# query count のデフォルトは 20
twitter.timeline(:friends, { :query => { :count => 5 } }).each do |twit|
  puts "#{twit['user']['screen_name']} [#{SimpleTwitter.format_time(twit['created_at'])}](#{twit['id'].to_s}):\n" +
       "  #{twit['text']}"
end

表示は以下のような感じ。

1
2
3
4
5
6
7
8
9
10
# >> delicious50 [2009/11/07 15:47:58](5501187604):
# >>   地球人ネットワークを創るアルク:スペースアルク http://bit.ly/lcfEp japan
# >> delicious50 [2009/11/07 15:47:56](5501187311):
# >>   findability.org - by Peter Morville http://bit.ly/15KKUP findability ia
# >> GuyKawasaki [2009/11/07 15:46:52](5501173816):
# >>   Guess who is a Glamour woman of the year? http://om.ly/bTqX
# >> tek_news [2009/11/07 15:46:49](5501173203):
# >>   HNews: Someone actually created a pyramid scheme for Twitter popularity. http://bit.ly/20gVa5
# >> GuyKawasaki [2009/11/07 15:46:46](5501172663):
# >>   The official rules of Shotgun http://om.ly/bTqU

Unfollowers をチェックするスクリプト

  • 標準ライブラリ以外の原材料
    • simple_twitter.rb
      上記にあるスクリプト

follows_unfollowers_check.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
require 'simple_twitter'

FOLLOWERS_CSV = 'followers.csv'
INDENT = ' ' * 2

def get_follower_user_list(twitter, cursor = -1)
  users = []
  twit_followers = twitter.followers(:query => { :cursor => cursor })

  twit_followers['users'].each do |follower|
    users << follower['screen_name']
  end

  next_cursor = (twit_followers['next_cursor'] ||= '0')
  unless next_cursor.to_i == 0
    users.concat(get_follower_user_list(twitter, next_cursor))
  end
  users
end

# 'user', 'password' には Twitter のログインアカウントとパスワードを指定
twitter = SimpleTwitter.new('user', 'password')
# Proxy を経由する必要がある場合
# twitter = SimpleTwitter.new('user', 'password',
#   { :http_proxy => 'proxy_server', :http_proxy_port => 8008 })

current_users = get_follower_user_list(twitter)

previous_users = []
if File.exist? FOLLOWERS_CSV
  begin
    previous_users = File.open(FOLLOWERS_CSV).read.split(',')
  rescue
    puts $!
    exit(9)
  end
end

unfollowers = previous_users - current_users
new_follow_users = current_users - previous_users

puts "Follow User Stats at #{SimpleTwitter.format_time(Time.now)}"
puts '=' * 50
puts "Current Follow Users count: %s" % current_users.size.to_s
puts "Unfollowers count: %s" % unfollowers.size.to_s

unfollowers.each do |user|
  puts "#{INDENT}#{user}\t#{SimpleTwitter.base_uri}/#{user}"
end

puts "New follow Users count: %s" % new_follow_users.size.to_s

new_follow_users.each do |user|
  puts "#{INDENT}#{user}\t#{SimpleTwitter.base_uri}/#{user}"
end

File.open(FOLLOWERS_CSV, 'w') { |f| f.write current_users * ',' }

結果は以下のような出力になる。(Emacs 上で rcodetools を利用した際の表示)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# >> Follow User Stats at 2009/11/07 15:03:01
# >> ==================================================
# >> Current Follow Users count: 65
# >> Unfollowers count: 7
# >>   SavvyExplorer     https://twitter.com/SavvyExplorer
# >>   talyaalove        https://twitter.com/talyaalove
# >>   59minlearning     https://twitter.com/59minlearning
# >>   lifefulfillment   https://twitter.com/lifefulfillment
# >>   follow4profit     https://twitter.com/follow4profit
# >>   Forex_Blog        https://twitter.com/Forex_Blog
# >>   VirtualTotte      https://twitter.com/VirtualTotte
# >> New follow Users count: 6
# >>   RecognitionApps   https://twitter.com/RecognitionApps
# >>   kazuyo_k          https://twitter.com/kazuyo_k
# >>   Dewina_Olivera    https://twitter.com/Dewina_Olivera
# >>   HP_AppSecurity    https://twitter.com/HP_AppSecurity
# >>   videotodaynews    https://twitter.com/videotodaynews
# >>   devtube           https://twitter.com/devtube

実行は、simple_twitter.rbfollows_unfollowers_check.rb を同じディレクトリにおいておいて、


$ ruby ./follows_unfollowers_check.rb

前回スクリプトを動かした時との差分が表示される。

正確にやろうと思うと、細目にこのスクリプトを動かしておかないといけない。前回動作時との間に follow/unfollow の両方を行ったアカウントは捉えられないので。
また、結果の出力は標準出力以外のもの、メールでの送信などにしておくのもよいかもしれない。

Issue/Bug トラッキングシステム/Git リポジトリ Unfuddle を試してみる

written by taka on November 4th, 2009 @ 07:51 AM  

Issue/Bug トラッキングシステムであり、Git/Subversion リポジトリの管理(Viewer だけではない)も行えるサービス。

Issue/Bug トラッキングシステムとソースリポジトリの連携は今では無くてはならない環境と(個人的には)なっている。
チケット駆動型の開発に慣れてしまうと、チケットを作成せずにソースに手を入れるというのは何となく心許ない。。

このような Issue 管理システムを使わずとも、リポジトリだけでもコメントは残せて、何故そのような commit を残したのかは記録され、振り返りは行える。
だが、その commit を残すまでの経緯、その commit を残すことになるトリガーを Issue 管理システム側のチケットを利用して記録し、経過を管理しておくことで、作業の漏れが少なくなり、一緒に作業を行っているメンバ間での途中経過の情報共有も行え、時間の節約に繋がる。

また、あれやこれやと空いた時間で手をつけている自分のような状況の者にとってすると、時間が経過した後に以前の作業に戻り易くなる。

Redmine は愛用しているのだが、現在内部ネットワークの閉じた環境で使用している。

  • 「あちら側」(クラウド)へ Issue 管理システムとソースリポジトリを移行したい
  • システムの運用管理はサービス提供者側にお任せしたい
    (もの作り、プロジェクトの運用に専念したい)

という思いがあり、Unfuddle を試してみることにした。

何故 Unfuddle なのか?は以下の記事に感化され、「agile reporting!」をやってみたくなったのもある。。

他にも同様のサービスはあるので、もしかするともっと素敵なサービスがあるかもしれない。現在他サービスの検証までは行っていない。

ここでは、

  • Issue/Bug トラッキングシステム Unfuddle のアカウント開設
  • Git リポジトリの登録
  • "Agile Reporting" のお試し (結局、次回の投稿に回すことに)

までをやってみる。

Unfuddle でアカウントを作成する

まずは Unfuddle でアカウントの作成を行う。

unfuddle-top

Unfuddle: Subversion Hosting, Git Hosting, Bug and Issue Tracking のトップページ "Start Unfuddling Now!" からアカウントを作成する。

unfuddle-new-account1

まずはお試しなので "Private FREE" で。本格的に使うとした場合、最低"Compact" のパッケージにはしたい。月 24 ドルか・・・。

unfuddle-create-account

Unfuddle で利用する自身のサイトの URI は、上記で指定する "Subdomain" になる。必要項目を入力して"Continue"。アカウントが作成される。

unfuddle-created-account

以下はアカウントのトップページになる Dashboard と、プロジェクト単位の Dashboard。

unfuddle-created-account

unfuddle-project-dashbord

Trac、Redmine などを使ったことのある方などは、さくさくと使い込なせるのではないかと思う。

利用方法については、本家のサイトのツアーを見て頂くのがよいかと思う。

Git リポジトリの登録と利用

Unfuddle のリポジトリには、Git、Subversion が利用できる。
ここでは Git を使用する。

Unfuddle にリモートリポジトリを作成し、ローカルリポジトリとの連携は、通常通り git コマンドを使用して行うことになる。 作成したリモートリポジトリとローカルリポジトリとのネットワークプロトコルは SSH になる。

手順としては、git でリモートリポジトリを利用する方法と同様。リモートリポジトリの管理部分だけ Unfuddle の UI を使って実施することになる。 ざっくりとした流れは以下の通り。

  1. リモートリポジトリを作成 - Unfuddle のサイトで
  2. SSH で利用する鍵ペアの作成 (作成していない場合) - ローカル環境
  3. ユーザアカウントに SSH 公開鍵を登録する - Unfuddle のサイトで
  4. ローカルリポジトリの準備 (作成していない場合) と反映する状態の FIX - ローカル環境
  5. ローカルリポジトリに Unfuddle のリモートリポジトリを追加 - ローカル環境
  6. ローカルリポジトリをリモートリポジトリに反映(push) - ローカル環境

アクセス管理を含めたリモートリポジトリの管理の手間が減るのが自前でやるのと比べてうれしい。

1. Unfuddle のサイトでリモートリポジトリを作成

Account、もしくは、Project の "Repositories" メニューから "Repositories" のページを開く。 "New Repository" をクリックし、新規のリポジトリ情報を入力する。

unfuddle-git-repository1

"Create Repository" を実行し、リポジトリを作成する。

unfuddle-git-repository2

2. SSH で利用する鍵ペアの作成 (作成していない場合)

まだ作成していない場合には、SSH の鍵ペアの作成を行う。SSH の鍵ペアの生成方法については、Unfuddle でドキュメントを用意してくれている。

3. ユーザアカウントに SSH 公開鍵を登録する

Unfuddle のサイトで 2. で作成した SSH 公開鍵(~/.ssh/id_rsa.pub)をユーザアカウントに紐付ける。

"People" タブで "Personal Settings" ページを開き、ユーザアカウントを選択する。

unfuddle-git-repository3

該当ユーザアカウントの"Edit"クリックし、"Edit Person" ページを開く。ページの下の方に"Public Keys"という項目があるので、"New Public Key.." をクリックする。

unfuddle-git-repository4

上記 2. で作成した SSH 公開鍵のファイル(通常は、~/.ssh/id_rsa.pub)を開き、その内容を"Value"にコピペする。

unfuddle-git-repository5

"Add" をクリックし、公開鍵を登録する。

unfuddle-git-repository6

4. ローカルリポジトリの準備 (作成していない場合) と反映する状態の FIX

管理するリポジトリをまだ準備していない場合には、準備しておく。

Unfuddle 固有の話ではなく、Git に関する話なのでここでは割愛する。以下の Unfuddle の提供しているドキュメントが参考になると思う。

5. ローカルリポジトリに Unfuddle のリモートリポジトリを追加

Unfuddle のリモートリポジトリの情報をローカルリポジトリに追加する。

まずは、ローカルの環境にて以下を実施。
<subdomain><abbreviation>は、以下の情報に置き換える。

  • <subdomain>: アカウントのサブドメイン
  • <abbreviation>: 1. でリモートリポジトリを作成した際に登録を行った "Avvreviation" の情報。

反映するローカルリポジトリのディレクトリに移動して実施。

1
2
3
$ cd /path/to/repository
$ git remote add unfuddle git@<subdomain>.unfuddle.com:<subdomain>/<abbreviation>.git
$ git config remote.unfuddle.push refs/heads/master:refs/heads/master

Git の設定情報に以下が追加される。

1
2
3
4
5
$ git config --list
...
remote.unfuddle.url=git@<subdomain>.unfuddle.com:<subdomain>/<abbreviation>.git
remote.unfuddle.fetch=+refs/heads/*:refs/remotes/unfuddle/*
remote.unfuddle.push=refs/heads/master:refs/heads/master

6. ローカルリポジトリをリモートリポジトリに反映(push)

準備は整ったので、ローカルリポジトリをリモートリポジトリに反映(push)する。

1
2
3
4
5
6
7
8
9
10
11
$ git push unfuddle master
The authenticity of host '<subdomain>.unfuddle.com (xxx.xxx.xxx.xxx)' can't be established.
RSA key fingerprint is xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '<subdomain>.unfuddle.com,xxx.xxx.xxx.xxx' (RSA) to the list of known hosts.
Counting objects: 307, done.
Compressing objects: 100% (282/282), done.
Writing objects: 100% (307/307), 214.83 KiB, done.
Total 307 (delta 111), reused 0 (delta 0)
To git@<subdomain>.unfuddle.com:<subdomain>/<abbreviation>.git
 * [new branch]  master -> master

Unfuddle のサイトで確認してみる。

unfuddle-git-repository7

リモートリポジトリからの clone は、以下の通りとなる。


$ git clone git@<subdomain>.unfuddle.com:<subdomein>/<abbreviation>.git

感想

感想の前に、、
「"Agile Reporting" のお試し」を誌面(?)の都合でまとめて書くことができなかったので、別の投稿に分けて記載する。

まだ感想を書けるまでは使い込なしていないが、現在使用している Redmine が持っている機能的なところでの比較としては、

  • リポジトリは Issue 管理システムからの参照ではなく、Issue 管理システム内で管理される
  • プロジェクトとリポジトリを M対N で紐付けることができる(できそう)

というところがうれしい。

Ruby gem 名称を指定して Emacs の Dired でそのライブラリのディレクトリを即座に開く

written by taka on October 28th, 2009 @ 06:50 AM  

やりたいことはタイトルの通りなのだが、Emacs を使用中に Ruby の gem 名称でそのライブラリのディレクトリを Dired で開くこと。

Ruby には素敵な gem が多くあるが、 付属の Readme、RDoc だけでは使い方がよくわからない、 どうやったらこのような素敵なことができるのかソースを見てみたい、 などの理由でソースを読んでみたくなる。

@bguthrie さんが Textmate でやっているのを見て、Emacs にも流用できるなと。

上記のソースで gem which <gem name> なんて便利なコマンドが使えるのを初めて知った。。

Emacs のディレクトリ操作には標準で Dired が利用できる。 C-x C-f(find-file) で該当する gem のディレクトリを開けばよいのだが、 ちょっとしたことではあるが、タブ補完が効くとは言え、そのパスを入力するのは少々面倒臭い。

また、仕事の関係上、Mac、Win と併用して使っているので、異なる環境でパスを思い出すのもメンドイ。。
gem の名称だけでわかれば直接そして即座に開けるようだと、メモリ容量が減ってしまっている自分の頭にもやさしい。

elisp は全くの初心者なので、かなり無理やり感も漂い、もっといい方法もありそうなのだが、とりあえず以下のような感じでいけそう。

1
2
3
4
5
6
(defun search-gem-path (name)
  (shell-command-to-string
    (format "gem which %s | ruby -n -e 'if /lib/; print $_[0, $_.rindex(%%[lib])]; end'" name)))
(defun find-ruby-gem (name)
  (interactive "sRuby gem libraray name: ")
  (find-file (search-gem-path name)))

上記スクリプトを .emacs.el なりに書いて M-x find-ruby-gem <gem 名称> とやることで、gem 名称に指定した gem のルートディレクトリが Dired で開かれる。

例えば、twitter gem を開く場合、M-x find-ruby-gem twitter として、

find-ruby-gem1

Enter!

find-ruby-gem2

ホントちょっとしたことではあるが、うれしい。。

ちなみに、標準ライブラリのソースを開く場合には、 devel-which ライブラリ(http://raa.ruby-lang.org/list.rhtml?name=devel-which)を使うとよい。

1
2
3
4
5
$ irb --prompt simple
>> require 'devel/which'
=> true
>> which_library 'net/http'
=> "/opt/local/lib/ruby/1.8/net/http.rb"

Ruby ActiveRecord の(超)簡易実装で学ぶメタプログラミング

written by taka on October 26th, 2009 @ 07:46 PM  

しっかり抑えておきたいと思うメタプログラミング

Ruby のメタプロミングは奥が深くて楽しいが、やり過ぎると暗黒面に堕ちてしまう。。 効果的な使い方を心掛けていきたい。
Rails でもよく利用されているように、CoC (Convention over Configuration) と組み合わせることで更に効果的となる。

なかなかとっつきにいところもあり、コンパクトにまとまった文書がないかと探していた。

上記サイトなどはコンパクトによくまとまっているように思う。

メタプログラミングに必要となる、通常はあまり使用しないメソッドをメタプログラミングツールボックスというかたちでリストアップし、その説明はドキュメントへのレファレンスで構成している。 最後にメタプログラミングの適用例として ActiveRecord の簡易実装をサンプルとして掲載している。

記載されているツールボックスの利用の前に、基本となる、

  • Ruby のクラス階層
    Object クラス、Module クラス、Class クラスと任意のクラスとの関連。
  • メソッド参照のルール

もしっかり抑えておく必要があるようには思う。その方が混乱が少なくて済む。。

個人的にはメタプログミングと言えば、why の Metaid が思い付くのだが、why はもう姿を現してくれないのだろうか・・・
"why's (poignant) guide to ruby" が以下のサイトで再掲されているようだ。

時間を見つけて以下のサイトを読み込んでみるのも良さそうだ。

また、"Metaprogramming Ruby" は非常に楽しみしている書籍で、まだベータブックではあるが、The Pragmatic Bookshelf | Metaprogramming Ruby を購入して読んでみようと思う。

Paolo Perrotta
Pragmatic Bookshelf ( 2010-01-31 )
ISBN: 9781934356470

と、今回のこのメモの中では、先に挙げた Ruby's Metaprogramming Toolbox で掲載されている ActiveRecord の簡易実装サンプルを SQLite 版で書いてみて、使用されているツールボックスのツールを確認してみたいと思う。

ActiveRecord の簡易実装 (ちょっとした検索だけ)

Ruby's Metaprogramming Toolbox では最後にメタプログミングの適用例として、 ActiveRecord の簡易実装を記載している。
これが MySQL 用のものであるので、もっと手軽に試せる SQLite 版にしてみた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
require 'rubygems'
require 'sqlite3'

# SQLite バージョン
# AR の CoC はとりあえず無視
class PoorManWithSqlite
  # クラスインスタンス変数に生成されたクラスのリストを保持する
  class << self; attr_reader :generated_classes; end
  @generated_classes = []

  def initialize(attributes = nil)
    if attributes
      attributes.each_pair do |key, value|
        instance_variable_set('@' + key, value)
      end
    end
  end

  def self.connect(database = 'data.db')
    @@db = SQLite3::Database.new(database)
    sql = <<-SQL
      select tbl_name
        from sqlite_master
       where type = 'table'
         and tbl_name != 'sqlite_sequence'
       order by tbl_name;
SQL
    
    @@db.execute(sql).flatten.each do |table_name|
      class_name = table_name.split('_').map { |word| word.capitalize }.join

      # Module#const_set を使用してテーブル用の新しいクラスを作成する
      @generated_classes << klass = Object.const_set(class_name,
                                                     Class.new(PoorManWithSqlite))

      klass.module_eval do
        @@fields = []
        @@table_name = table_name
        def fields; @@fields; end
      end

      # テーブルのフィールドのリストを収集し、
      # それらに対する getter/setter を用意する。
      @@db.table_info(table_name) do |field|
        # getter/setter の追加
        klass.send :attr_accessor, field['name'].to_sym
        # field セットに追加
        klass.module_eval { @@fields << field['name'] }
      end
    end
    @@db
  end

  def self.close
    @@db.close
  end

  # id でレコードを取得する
  def self.find(id)
    # SQLite3::Database#execute2 は、最初の行としてカラム名称の配列を返す。
    result = @@db.execute2("select * from #{@@table_name} where id = #{id} limit 1")
    attributes = Hash[*result.transpose.flatten]
    new(attributes) if attributes
  end

  # 全てのレコードを取得する
  def self.all
    results = @@db.execute2("select * from #{@@table_name}")
    fields = results.shift
    records = []
    results.each do |result|
      records << new(Hash[*fields.zip(result).flatten])
    end
    results
  end
end

PoorManWithSqlite.connect
table_classes = PoorManWithSqlite.generated_classes # => [Departments, Employees]
table_classes[0].superclass               # => PoorManWithSqlite
employee = Employees.find(1)              # => #<Employees:0x11f20d8
                                                 @department_id=nil,
                                                 @name="taka",
                                                 @id="1">
employee.name                             # => "taka"
employees = Employees.all                 # => [["1", "taka", nil],
                                                ["2", "goro", nil]]
PoorManWithSqlite.close                             # => true

上記の実行にあたっては、簡単な DB を用意しておく必要がある。

  • create-table.sql

ファイルの内容は以下の通り。

1
2
3
4
5
6
7
8
9
create table departments (
  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  name varchar(255)
);
create table employees (
  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
  name varchar(255),
  department_id integer
);

data.db を作成し、上記 DDL 文を流し込む。


$ sqlite3 data.db < creat-table.sql

ポイントメモ

幾つかポイントをメモ。

  1. 動的なクラスの生成(しかもサブクラス)
  2. 動的なアクセサの生成とコンストラクタによる値のセット
  3. クラスインスタンス変数の利用とそのアクセサの生成
  4. Object#send を使った private メソッドへのアクセス

1. 動的なクラスの生成(しかもサブクラス)

Module#const_set を利用して、テーブルの名前から動的に PoorManWithSqlite クラスのサブクラスとなる各テーブルのクラスを生成している。

1
2
3
# Module#const_set を使用してテーブル用の新しいクラスを作成する
@generated_classes << klass = Object.const_set(class_name,
                                               Class.new(PoorManWithSqlite))

Module#const_set(name, value) -> object

モジュールに name で指定された名前の定数を value という値に定義し、value を返す。

1
2
3
C = Object.const_set('Con', 10)
Con                             # => 10
C                               # => 10

Ruby の場合、クラス名は全て定数となる。name にテーブル名から生成した名称のクラス名を指定し、 value には、PoorManWithSqlite のサブクラスを指定している。

その value の指定に使用されているのは Class::new メソッド。Class の new メソッドには、インスタンスメソッドとクラスメソッドがある。 上記で利用しているのはクラスメソッド(Class::new)の方になる。

Class::new(superclass = Object) -> Class
Class::new(superclass = Object) {|klass| ... } -> Class

新しく superclass の無名のサブクラスを生成する。

1
2
3
4
5
class SuperAClass; end
sub_k = Class.new(SuperAClass)  # => #<Class:0x2533c>

sub_k.class                     # => Class
sub_k.superclass                # => SuperAClass

コードブロックを預けてメソッドを定義することもできる。

1
2
3
4
5
6
7
8
k = Class.new(SuperAClass) do |klass|
  def hoge
    'hoge hoge'
  end
end

o = k.new                       # => #<#<Class:0x273e4>:0x273a8>
o.hoge                          # => "hoge hoge"

各テーブルのクラスを PoorManWithSqlite クラスのサブクラスとすることで、各テーブルクラスにおいて PoorManWithSqlite の機能が利用できる。

2. 動的なアクセサの生成とコンストラクタによる値のセット

動的なアクセサを用意しているところ。

1
2
3
4
5
6
7
 # テーブルのフィールドのリストを収集し、それらに対する getter/setter を用意する。
 @@db.table_info(table_name) do |field|
   # getter/setter の追加
   klass.send :attr_accessor, field['name'].to_sym
   # field セットに追加
   klass.module_eval { @@fields << field['name'] }
 end

テーブルのフィールドをイテレートし、そのテーブルクラスの読み書き両用のインスタンス変数を生成している。

次に値をセットしているところ。

1
2
3
4
5
6
7
def initialize(attributes = nil)
  if attributes
    attributes.each_pair do |key, value|
      instance_variable_set('@' + key, value)
    end
  end
end

コンストラクタで引数にフィールド名を key、そのフィールドの値を value とした Hash を受け取り、 Object#instance_variable_set で値のセットを行う。'@' を文字列連結しているところはちょっとカッコ悪いが致し方ない。

上記 initialize メソッドが利用される実際に値をセットしているところは以下の箇所となる。

1
2
3
results.each do |result|
  records << new(Hash[*fields.zip(result).flatten])
end

Hash[*fields.zip(result).flatten] は検索結果の配列からフィールド名を key、そのフィールドの値を value とした Hash を生成している。

3. クラスインスタンス変数の利用とそのアクセサの生成

各テーブルクラスは、PoorManWithSqlite クラスを継承する。

継承の際のクラス変数の扱いには注意が必要だ。
1.8 系まではクラス変数は継承される(1.9 はまだ触っていない。。)。

Database への接続の参照は唯一のインスタンスとして持たせたいので、クラス変数が適している。
ただ、作成されたテーブルクラスの管理情報を各テーブル毎に持たせても仕方ない。このサンプルでは PoorManWithSqlite クラスのクラスインスタンス変数にその情報を保持している。

1
2
3
# クラスインスタンス変数に生成されたクラスのリストを保持する
class << self; attr_reader :generated_classes; end
@generated_classes = []

アクセサは、PoorManWithSqlite クラスオブジェクトの特異クラスで記述する必要がある。

4. Object#send を使った private メソッドへのアクセス

1
2
3
4
5
6
7
 # テーブルのフィールドのリストを収集し、それらに対する getter/setter を用意する。
 @@db.table_info(table_name) do |field|
   # getter/setter の追加
   klass.send :attr_accessor, field['name'].to_sym
   # field セットに追加
   klass.module_eval { @@fields << field['name'] }
 end

klass というのは、PoorManWithSqlite を継承したテーブル毎のクラスなのだが、このクラスの attr_accessor メソッドには、klass をレシーバとしてはアクセスできない。 Object#send を使うことにより、アクセスコントロールをバイパスしている。

例示のため使っているのだと思うが、その下にある Module#module_eval のブロックの中で一緒に書いてもよいと思う。

1
2
# getter/setter の追加とfield セットの追加
klass.module_eval { attr_accessor field['name'].to_sym; @@fields << field['name'] }

Twitter を始めてみた

written by taka on September 11th, 2009 @ 10:03 PM  

Twitter を3日ほど使ってみた感想

最近、Twitter を使い始めてみた。

まずは、Twitter って何?というところで、以下のサイトにざっと目を通すと概要が掴める。

呟くだけで何が楽しいのか?と斜に構え、敬遠していた Twitter だが、始めるきっかけになったのが以下の記事。

Feedburner の共同ファウンダー兼 CEO のDick Costoloが、Twitterの新 COO になったことに関する記事なのだが、この記事のタイトルは、Steve Gillmor 氏の以下の記事に発端がある。

It’s time to get completely off RSS and switch to Twitter.

...

All my RSS feeds are in Google Reader. I don’t go there any more. Since all my feeds are in Google Reader and I don’t go there, I don’t use RSS anymore.

自分にとってみたら、RSS Reader は今や生活の一部(?)になっているほど。それが Twitter にとって変わる? ちょっと飛ばし過ぎの記事にも思えたのだが、まぁそこまで言うのであれば・・・と使ってみる気になった。

使って3日程経過したが、使ってみた感想としては、RSS Reader を捨てるまでにはいかないが、なかなかどうして、

  • リアルタイム性
  • 気になる人(企業)の発言を気軽に追っかけることができる
  • 思いもしなった情報が入ってくる

というところはよかった。感覚的には、ラジオを"見る"感覚に近い気がした。

また、能動的な使い方としては、ちょっとしたメモを含めた自身のライフログ的なものを Twitter に集約できることがわかった。これまでその手の目的には、ChangeLog などにメモを書き殴っていたのだが、Twitter でいいかもしれない。

個人的な利用ではこんなものだが、企業が使うとした場合、カスタマーサービスでの利用に効果がありそうな気がする。

ただ、まだ3日。まだまだ使い込なせていないが、Twitter 自体は非常にシンプルな設計になっているので、それを使う人の使い方によってはもっと面白い効果を出すことができそうだ。

まずはどういうものか知りたいため、この数日は、なるべく Twitter に触れる時間を作っていたが、時間は有限。。やるべき作業も他にもあるので、今後はバランス良くお付き合いしていきたい。。

フォローする人を探す

誰をフォローするかで、Twitter の楽しさは変わってくると思う。
以下のようなまとめサイトがある。

Twitter への企業の取り組み

まだまだ情報収集できていないが、ここ数日だけでも以下のような内容が耳に入ってきた。

Salesforce の以下のデモは興味深い。

関連ツールいろいろ

Twitter には関連ツールがいろいろあるようだ。ここ数日使っていて便利だったものをメモしておく。

Twitter クライアント

Web サイトで完結できるのだが、便利なクライアントツールがある。

自分の場合、Emacs をメインで使っているので、elisp のクライアントが欲しかった。以下のものが使い勝手がよかった。

emacs-twittering-mode

ただ、ダイレクトメッセージが見られないのが玉に瑕。

Emacs 何それ?・・・という方には、

などが評価が高い模様。Adobe AIR で作成されているので、プラットフォームは選ばない。iPhone でも利用可能なようだ。

確かに、カラム機能とフィルータ機能がすばらしい。

デフォルトでは日本語の呟きが表示されないようなので、[Settings]->[Colors/Font] で 'Original Font' から 'International Font/TwitterKey' へ変更を行っておく。

tweetdeck-setting

Twitter のつぶやきをブログ形式で保存してくれる - Twilog

Twitter の過去ログはブラウザで全て見ることはできない。Twilog は Twitter のつぶやきをブログ形式で保存してくれる。
ライフログ的な使い方を考えていたので、これは助かる。

保存されたブログは、http://twilog.org/twitterのアカウント で公開される。

ブログの更新を Twitter に流す

twitterfeed

ブログの更新を一定間隔で Twitter に流してくれるサービス。

まとめて配信された RSS を twitterfeed で流すという方法もあるようだ。

twitbackr

更新 Ping を送ってやることで、Twitter へ更新情報をポストする。

その他いろいろ

関連するツールは他にもかなりの数があるので、以下のサイトなどで確認してみるのもよいかと思う。

Ruby で REST リソースにアクセスするための REST Client

written by taka on September 7th, 2009 @ 01:50 AM  

Ruby の場合、組み込みライブラリの net/http を使用すれば、如何ようにでもできるのだが、もう少し扱い易くシンプルなインターフェイスで操作したい。これまで、open-uri を拡張した rest-open-uri を使用していたのだが、heroku のクライアントで使われていた REST Client を使ってみることにした。

A simple REST client for Ruby, inspired by the Sinatra‘s microframework style of specifying actions: get, put, post, delete.

gem でインストールできる。

1
2
3
4
5
6
$ sudo gem install rest-client
Password:
Successfully installed rest-client-1.0.3
1 gem installed
Installing ri documentation for rest-client-1.0.3...
Installing RDoc documentation for rest-client-1.0.3...

REST リソースとのやりとり

REST リソースを提供するサーバの準備

まずは、サーバ側の準備をしておく。Rails プロジェクトで作成しておく。今回は、認証部分は考慮していない。

Rails プロジェクトを作成。

1
2
$ rails -d sqlite3 rest-test
$ cd rest-test

Scaffold を使って簡単な記事の投稿、表示、変更、削除ができるアプリケーションの雛形を作成する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ script/generate scaffold Article title:string content:text published_at:datetime
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/articles
      exists  app/views/layouts/
      exists  test/functional/
      exists  test/unit/
      create  test/unit/helpers/
      exists  public/stylesheets/
      create  app/views/articles/index.html.erb
      create  app/views/articles/show.html.erb
      create  app/views/articles/new.html.erb
      create  app/views/articles/edit.html.erb
      create  app/views/layouts/articles.html.erb
      create  public/stylesheets/scaffold.css
      create  app/controllers/articles_controller.rb
      create  test/functional/articles_controller_test.rb
      create  app/helpers/articles_helper.rb
      create  test/unit/helpers/articles_helper_test.rb
       route  map.resources :articles
  dependency  model
      exists    app/models/
      exists    test/unit/
      exists    test/fixtures/
      create    app/models/article.rb
      create    test/unit/article_test.rb
      create    test/fixtures/articles.yml
      create    db/migrate
      create    db/migrate/20090906091207_create_articles.rb

マイグレーションを行い、スキーマを生成しておく。

1
2
3
4
5
6
$ rake db:migrate
(in /Users/taka/work/rest-test)
==  CreateArticles: migrating =================================================
-- create_table(:articles)
   -> 0.0032s
==  CreateArticles: migrated (0.0035s) ========================================

サーバの起動。

1
2
3
4
5
$ script/server 
=> Booting Mongrel
=> Rails 2.3.2 application starting on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server

ブラウザから記事を2つほど登録しておく。

rest-article-list-1

リソースを取得する

REST Client の RestClient.get で Article の一覧を取得する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ irb
>> require 'rubygems'
=> true
>> require 'rest_client'
=> true
>> RestClient.get 'http://localhost:3000/articles'
=> "<?xml version="1.0" encoding="UTF-8"?>\n
      <articles type="array">\n
        <article>\n
          <content>This is the first article!!</content>\n
          <created-at type="datetime">2009-09-06T09:13:46+09:00</created-at>\n
          <id type="integer">1</id>\n
          <published-at type="datetime">2009-09-06T09:13:00+09:00</published-at>\n
          <title>First Article</title>\n
          <updated-at type="datetime">2009-09-06T09:13:46+09:00</updated-at>\n
        </article>\n
        <article>\n
          <content>This is the second article!\r\n</content>\n
          <created-at type="datetime">2009-09-06T18:17:35+09:00</created-at>\n
          <id type="integer">2</id>\n
          <published-at type="datetime">2009-09-06T18:17:00+09:00</published-at>\n
          <title>Second Article</title>\n
          <updated-at type="datetime">2009-09-06T18:17:35+09:00</updated-at>\n
        </article>\n
      </articles>\n"

レスポンスとなっている XML の解析には、REXML を使用する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>> require 'rexml/document'
=> true
>> doc_root = REXML::Document.new(RestClient.get('http://localhost:3000/articles')).root
=> articles typearray .... 
>> doc_root.elements.each('//article') do |article|
?>   puts [
?>     article.elements['id'].text,
?>     article.elements['title'].text,
?>     article.elements['content'].text
>>   ].join(':')
>> end
1:First Article:This is the first article!!
2:Second Article:This is the second article!
=> [article .... , article .... ]

この後、いろいろ試してみるにあたり、投稿一覧を取ってくるメソッドがあると便利なので、定義しておく。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>> require 'ostruct' # OpenStruct を使用するため
=> true
>> require 'time' # Time.parse を使用するため
=> true
>> def get_article_list
>>   doc_root = REXML::Document.new(RestClient.get('http://localhost:3000/articles')).root
>>   articles = []
>>   doc_root.elements.each('//article') do |article|
?>     art = OpenStruct.new
>>     art.resource_id = article.elements['id'].text.to_i
>>     art.title = article.elements['title'].text
>>     art.content = article.elements['content'].text
>>     art.published_at = Time.parse article.elements['published-at'].text
>>     articles << art
>>   end
>>   articles
>> end

get_article_list で投稿一覧を取得する。

1
2
3
>> get_article_list
=> [#<OpenStruct published_at=Sun Sep 06 09:13:00 +0900 2009, resource_id=1, content="This is the first article!!", title="First Article">,
    #<OpenStruct published_at=Sun Sep 06 18:17:00 +0900 2009, resource_id=2, content="This is the second article!\n", title="Second Article">]

リソースを更新する

RestClient.put を使って2番目の投稿の本文を更新してみる。

1
2
3
4
5
6
>> article2 = get_article_list[1]
=> #<OpenStruct published_at=Sun Sep 06 18:17:00 +0900 2009, resource_id=2, content="This is the second article!\n", title="Second Article">
>> RestClient.put(
?>   "http://localhost:3000/articles/#{article2.resource_id.to_s}",
?>   :article => { :content => "#{article2.content} Changed" })
RestClient::RequestFailed: HTTP status code 422

エラーコード 422 は、Unprocessable Entity になる。サーバ側のログを覗いてみると、

1
2
3
4
Processing ArticlesController#update (for 127.0.0.1 at 2009-09-07 00:13:06) [PUT]
  Parameters: {"article"=>{"content"=>" Changed"}, "id"=>"2"}

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

送信したリクエストの中に、CSRF 対策のために Rails で使用されている AuthenticityToken が無いためだ。

今回は、AuthenticityToken のトークンチェックは行わずに、検証を続ける。

app/controllers/articles_controller.rb に以下の記述を追加し、トークンチェックを省略する。

1
2
class ArticlesController < ApplicationController
  skip_before_filter :verify_authenticity_token

再度クライアントから更新を行う。

1
2
3
4
5
6
7
>> RestClient.put(
?>   "http://localhost:3000/articles/#{article2.resource_id.to_s}",
?>   :article => { :content => "#{article2.content} Changed" })
=> " "
>> get_article_list
=> [#<OpenStruct published_at=Sun Sep 06 09:13:00 +0900 2009, resource_id=1, content="This is the first article!!", title="First Article">,
#<OpenStruct published_at=Sun Sep 06 18:17:00 +0900 2009, resource_id=2, content="This is the second article!\n Changed", title="Second Article">]

更新されたようだ。

サーバ側のログも確認してみる。

1
2
3
4
5
6
Processing ArticlesController#update (for 127.0.0.1 at 2009-09-07 00:22:03) [PUT]
  Parameters: {"article"=>{"content"=>"This is the second article!\n Changed"}, "id"=>"2"}
  Article Load (0.2ms)   SELECT * FROM "articles" WHERE ("articles"."id" = 2) 
  Article Update (0.7ms)   UPDATE "articles" SET "content" = 'This is the second article!
 Changed', "updated_at" = '2009-09-07 00:22:03' WHERE "id" = 2
Completed in 12ms (View: 1, DB: 1) | 200 OK [http://localhost/articles/2]

リソースを新規に追加する

RestClient.post を使って新規に投稿する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>> RestClient.post(
?>   "http://localhost:3000/articles",
?>   :article => { :title => "Third Article", :content => "This is third article!!",
?>     :'published_at(1i)' => "2009", :'published_at(2i)'=> "9", :'published_at(3i)'=> "6",
?>     :'published_at(4i)' => "22", :'published_at(5i)' => "10"})
=> "<?xml version="1.0" encoding="UTF-8"?>\n
      <article>\n
        <content>This is third article!!</content>\n
        <created-at type="datetime">2009-09-07T00:33:11+09:00</created-at>\n
        <id type="integer">3</id>\n
        <published-at type="datetime">2009-09-06T22:10:00+09:00</published-at>\n
        <title>Third Article</title>\n
        <updated-at type="datetime">2009-09-07T00:33:11+09:00</updated-at>\n
      </article>\n"

新規に追加されたようだ。DB の id も新規に 3 が採番されている。

サーバ側のログも確認してみる。

1
2
3
4
Processing ArticlesController#create (for 127.0.0.1 at 2009-09-07 00:33:11) [POST]
  Parameters: {"article"=>{"title"=>"Third Article", "published_at(1i)"=>"2009", "content"=>"This is third article!!", "published_at(2i)"=>"9", "published_at(3i)"=>"6", "published_at(4i)"=>"22", "published_at(5i)"=>"10"}}
  Article Create (37.3ms)   INSERT INTO "articles" ("updated_at", "title", "content", "published_at", "created_at") VALUES('2009-09-07 00:33:11', 'Third Article', 'This is third article!!', '2009-09-06 22:10:00', '2009-09-07 00:33:11')
Completed in 51ms (View: 2, DB: 37) | 201 Created [http://localhost/articles]

当然ながら、ブラウザで確認してみても、これまでの作業の内容が確認できる。

rest-article-list-2

リソースを削除する

RestClient.delete を使って投稿を削除する。 1番目に投稿したリソースの削除を行う。

1
2
3
4
5
6
7
>> article1 = get_article_list[0]
=> #<OpenStruct published_at=Sun Sep 06 09:13:00 +0900 2009, resource_id=1, content="This is the first article!!", title="First Article">
>> RestClient.delete "http://localhost:3000/articles/#{article1.resource_id.to_s}"
=> " "
>> get_article_list
=> [#<OpenStruct published_at=Sun Sep 06 18:17:00 +0900 2009, resource_id=2, content="This is the second article!\n Changed", title="Second Article">,
    #<OpenStruct published_at=Sun Sep 06 22:10:00 +0900 2009, resource_id=3, content="This is third article!!", title="Third Article">]

確かに削除されている。

サーバ側のログも確認する。

1
2
3
4
5
Processing ArticlesController#destroy (for 127.0.0.1 at 2009-09-07 00:47:24) [DELETE]
  Parameters: {"id"=>"1"}
  Article Load (0.2ms)   SELECT * FROM "articles" WHERE ("articles"."id" = 1) 
  Article Destroy (0.6ms)   DELETE FROM "articles" WHERE "id" = 1
Completed in 10ms (View: 1, DB: 1) | 200 OK [http://localhost/articles/1]

REST Client のそれ以外の特徴

HTTP のベーシック認証

HTTP のベーシック認証にも対応している。リソースの前に <account id>:<password>@ を追加してあげればよい。

以下は、delicious で最後にブックマークを追加した時間を取得する API を叩いた例。

1
2
3
4
>> RestClient.get 'https://taro:password@api.del.icio.us/v1/posts/update'
=> "<?xml version="1.0" encoding="UTF-8"?>\n
      <update time="2009-09-06T07:23:43Z" inboxnew="0"/>\n
      <!-- fe10.feeds.del.ac4.yahoo.net compressed/chunked Sun Sep  6 09:09:45 PDT 2009 -->\n"

ロギング

RestClient.log= にて、ログのファイル名、または stdoutstderr も指定できる。

以下は、stdout を指定した例。

1
2
3
4
5
6
7
8
>> RestClient.log = 'stdout'
=> "stdout"
>> RestClient.get 'https://taro:password@api.del.icio.us/v1/posts/update'
RestClient.get "https://drtaka:fastone13@api.del.icio.us/v1/posts/update"
# => 200 OK | text/xml 179 bytes
=> "<?xml version="1.0" encoding="UTF-8"?>\n
      <update time="2009-09-06T07:23:43Z" inboxnew="0"/>\n
      <!-- fe10.feeds.del.ac4.yahoo.net compressed/chunked Sun Sep  6 09:09:45 PDT 2009 -->\n"

ヘッダーの取得

RestClient::Response#headers でレスポンスの HTTP ヘッダーの取得が可能。

1
2
3
4
5
6
7
8
>> (RestClient.get 'http://localhost:3000/articles').headers
=> {:etag=>""33ccfb63496b854f2bd158a91962a48a"",
    :x_runtime=>"13",
    :content_type=>"application/xml; charset=utf-8",
    :cache_control=>"private, max-age=0, must-revalidate",
    :content_length=>"793",
    :connection=>"close",
    :date=>"Sun, 06 Sep 2009 16:17:11 GMT"}

その他

それ以外にも、プロキシの指定、restclient コマンドでの操作、など扱い易いインターフェイスを揃えてくれている。以下の readme を参照。

参考サイト

Emacs で Cucumber の Feature を編集し易くする feature-mode.el

written by taka on August 17th, 2009 @ 01:13 AM  

Cucumber の Feature の記述を楽にしてくれる elisp。

インストールと設定の例は以下の通り。
($HOME/.emacs.d 配下に追加の elisp は突っ込んでいる。)

1
2
$ cd ~/.emacs.d
$ git clone git://github.com/michaelklishin/cucumber.el.git cucumber

.emacs には以下を追加。

1
2
3
(add-to-list 'load-path "~/.emacs.d/cucumber")
(autoload 'feature-mode "feature-mode" "Mode for editing cucumber files" t)
(add-to-list 'auto-mode-alist '("\\.feature$" . feature-mode))

git clone したディレクトリの中に snippets も用意されているので、snipet の laod directory にパスを追加しておく。
yasnippet の設定は、下記のページなどが参考になる。

HotPads の Amazon EC2/S3 を利用したサービスの実際のコスト

written by taka on August 2nd, 2009 @ 03:32 AM  

"クラウド"という言葉はちょっと前まではあまり好きではなかった。 その言葉に対しての実体がイマイチイメージがつき辛く、ボンヤリしたものであったためだ。

(なんちゃって)コンサルタントの方々がその言葉を口にして、予算を握っている方々に、 "クラウド" という言葉通りのボンヤリとした絵の書かかれたスライドを片手に、 もっともらしい話をしているのを各所で聞いていた影響もあるのかもしれない。。

ただ、最近はサービスの実用化が進んでおり、個人的にも興味を惹かれ始めていることもあり、現実的な話がアンテナにひっかかってくるようになった。

HotPads が示した AWS の具体的な利用例とそのコスト

この記事は結構インパクトがあった。

上記のスライドの中で、サービス規模、システム構成、実際にかかっている月々のコストが具体的に示されている。

運用しているサービスの規模としては、

  • 800,000 訪問者/月
  • 4.5 百万ページビュー/月
  • 日々アップデートされる 3.5 百万件の不動産情報

となっている。めちゃくちゃ大規模というものではないが、かといって小規模という枠でもない。

上記スライドの内容をうまくまとめてくれているコラムが以下のコラム。

上記からコストの詳細部分を引用する。

Costs

・EC2 - $7400/month - run 20 of various size instances at anyone time. Most work is in the background processing of images, not web serving.

  • $150: 2 Small HAProxy Load Balancers - 2 for failover, these have the elastic IPs, round robin DNS point at the elastic IPs.
  • $1,200: 3-5 Large Tomcat Web Servers - an array of 3 run at night and 5 during the day.
  • $1,500: 5 Large Tomcat Job Servers
  • $900: 1 X-Large 1 Large Index Server - used to power property search and have several GB of RAM for the JVM
  • $1,200: 1 X-Large 2 Large MySQL masters
  • $1,200: 1 X-Large 2 Large MySQL slaves
  • $300: 1 Large Messaging Server ActiveMQ - will be replaced with SQS
  • $300: 1 Large Map tile creation servers Tilecache
  • $600: Development/testing/migration/ servers

・S3 - $1500/month - few hundred million objects for files for maps and real-estate listing photos. 4TB of database backup stored as EBS diffs ($600/month).

・Elastic Block Storage - $500/month

・CloudFront - $460/month - is used to serve static files and map files throughout the world. It serves static files, map tiles, and listing photos.

・Elastic IP Addresses - $8/month

・RightScale - $500/month - used for management and deployment.

現在 100円 を切っているが、1ドルを100円計算とした場合、約110万/月で上記のサービス規模、システムを稼動させている。

金額だけの観点からいくと、以前プロバイダに支払っていた金額と同じ金額だということだ。このコストについて、同じコラムで Todd Hoff 氏は以下の感想を述べている。

I found this is a little surprising as I thought the cloud would be more expensive, but they only pay for what they need instead of having to over provision for transient uses like testing. And some servers aren't necessary anymore as EBS handles backups so database slave servers are no longer required.

私にとってはこのことはちょっとした驚きだった。クラウドはもっとコストのかかるものだと思っていたのだが、テスト環境のような一時的な利用における備えをしておく必要はなく、それが必要な時に必要な金額だけを払えばいいのだ。また、(実際の運用では必要になる)幾つかのサーバはもはや必要ないのだ。例えば、EBS がバックアップは行ってくれるので、スレーブのデータベースサーバはもはや必要なくなっている。

(括弧内の言葉は自分の方で補記した。)

この HotPads の例から学んだこととして、"Lessons Learned" の項の中で Todd Hoff 氏は幾つか挙げている。印象に残ったところを。

・Overall cost is about the same as with previous hosting site but the overall speed of development and ease of management is night and day different. Getting more servers and lots more flexibility.

・全体のコストは、以前のホスティングサイトとほぼ同じ。だが、開発のスピードと管理の容易さは、夜と昼ほどの違いがある。より多くのサーバを得られて、より多くの柔軟性を得る。

インフラのスペシャリスト達が作ったプラットフォームで、そのプラットフォームの運用は任せられる。 ソフトウェアと同様、インフラ部分においても大変なところは初期の構築はもちろんだが、その後の運用。 当然ながらコストの中には"もの"だけではなく、人件費(その人間力/技術力含めて)のコストが含まれる。

セキュリティ、もろもろの規定含めて考えないといけないところはあるかと思うが、PaaS (Platform as a Service) という"クラウドサービス"はとても魅了的に映る。

これまでのように、3年後、5年後にはシステム開発のあり方はまた変化しているのだろう・・・と思わざるを得ない。その手法だけでなく、業界の構造としても。

参考サイト

Radiant CMS - フレキシブルな Ruby On Rails ベースの CMS

written by taka on July 28th, 2009 @ 01:44 AM  

これまで Radiant に何度か触れながら、そもそも Radiant って何?というメモを残していなかったので、改めて。

Radiant とは?

Radiant is a no-fluff, open source content management system designed for small teams. It is similar to Textpattern or MovableType, but is a general purpose content management system (not just a blogging engine).

"no-fluff" という表現は時々見かけるのだが、どのようなニュアンスなのだろう。。

Radiant は Ruby On Rails をベースに作成されたオープンソースの CMS (Content Management System) アプリケーションになる。
Radiant が提供する管理画面を利用してブラウザからコンテンツの作成、編集などのコンテンツ管理が行える。

コンテンツの作成には、HTML 以外に Radiant タグという便利なタグが使用できるようになっており、条件文、コンテンツの繰り返し処理など、静的なページだけでなく動的なページの作成も可能となる。
また、必要に応じてエクステンションという拡張モジュールを導入することで追加のタグや機能が利用可能となり、フレキシブルなサイト構築が可能となる。

必要とする機能がエクステンションとして提供されていなければ、エクステンションを自分で作成してしまえばよい。 Ruby On Rails を扱ったことがあれば、そこまで難しい話ではない。

Radiant の概要を理解するのは、デモを使ってみるのが手っ取り早い。

現在 2009/06/14 にリリースアナウンスが行われた v.0.8.0 が最新バージョンであるが、Radiant 自体は 2006年(確か)から開発が続いており、歴史もあり、完成度も高い。

海外では結構実績のあるアプリケーションになるのだが、日本では Ruby 本家のサイトで採用されていたりはするのだが、メジャーな存在だとはいい難い。。その理由については、思いつくところをこのメモの後ろ方で考えてみた。

Radiant の特徴

上記ページにて 10 個の特徴が挙げられている。

  1. シンプルな管理者サイトのインターフェイス

    Built from the ground up to be as simple as possible, Radiant features an elegant administrative interface that centers around three key components: pages, snippets, and layouts.

    可能な限りシンプルに構築されている。3つのキーコンポーネント (ページ、スニペット、レイアウト)を軸としたエレガントな管理者サイトのインターフェイスに特徴がある。

  2. ページ (Pages)

    Pages contain the meat of the content for a Web site and may use Markdown, Textile, or plain HTML. Pages are composed of multiple parts such as a body and sidebar.

    ページは、Web サイトのコンテンツを意味する。その記述には、Markdown、Textile、プレーンな HTML の利用が可能となる。ページは、bodysidebar といった複合的なパート(multiple part)によって構成される。

  3. スニペット (Snippets)

    Content that is used in multiple places can be stored in a snippet. Snippets are very similar to PHP includes or Rails partials.

    いろいろなところで利用されるコンテンツは、スニペットの中にストアしておく。スニペットは、PHP の includesや Rails の partials によく似ている。

  4. レイアウト (Layouts)

    Layouts generally contain most of the HTML for a page's design. Layouts can render pages parts in any way they choose. One layout could choose to render the body and sidebar of a page, while another layout (a print layout) could render only the body.

    レイアウトは、通常、ページのデザインのための HTML の大半を含む。レイアウトは、レイアウトに記載されるページのパーツをレンダリングする。あるレイアウトでは、bodysidebar をレンダリングし、別のレイアウト(印刷用レイアウト)には、body だけをレンダリングさせることができる。

  5. フレキシブルなサイト構成

    Unlike many other blogging engines Radiant allows you to arrange pages according to any hierarchy. A Weblog in Radiant can be as simple as a collection of child pages underneath a parent page.

    多くの他のブログエンジンとは異なり、Radiant は階層構造によってページの管理を行える。Radiant におけるウェブログは、"親ページ"の下に位置する単純な"子ページ"のコレクションとなる。

  6. Radius テンプレート言語

    Radiant has a special macro language (similar to HTML) called Radius which makes it easy to include content from other pages, iterate over page children, and display content conditionally. Radius tags are available in pages, snippets, and layouts.

    Radiant は、Radius という特別なマクロ言語(HTML に似ている)を使用できる。そのマクロ言語は、他のページからのコンテンツを含めることや、"子ページ" をイテレートすることや、条件に合わせてコンテンツを表示する、といったことを容易に可能とする。Radius タグは、ページ、スニペット、レイアウトで利用可能。

  7. カスタムテキストフィルター

    Radiant ships with support for Markdown and Textile, but developers can easily create text filters for any markup language such as RDoc, BBCode, or Structured Text.

    Radiant は Markdown と Textile のサポートを標準装備としている。とはいえ、開発者は RDoc、BBCode、構造化されたテキストのようなマークアップ言語のテキストフィルターを容易に作成するこができる。

  8. インテリジェントなページキャシング

    Radiant includes an intelligent caching mechanism which allows content to be cached for a maximum of 5 minutes. This ensures that content is always fresh and provides an optimal level of performance.

    Radiant はコンテンツを最大5分間キャッシングするインテリジェントなキャッシュの仕組みを持っている。このことは、コンテンツがいつも新鮮であることを保証しつつ、最適なレベルのパフォーマンスを提供する。

  9. Ruby On Rails で構築されている

    Radiant is built using Ruby on Rails. This means it’s easy for developers to extend Radiant because it’s built on a widely accepted (and understood) Web application development platform.

    Radiant は Ruby On Rails を使用して構築されている。このことは、開発者にとっては Radiant を容易に拡張できることを意味する。なぜなら、Ruby On Rails は、広く受け入れられ(かつ理解されている) Web アプリケーション開発プラットフォームだから。

  10. MIT ライセンス

    Radiant is licensed under the MIT License. This means that Radiant is free for commercial and non-profit use. It also means that you are free to modify and distribute Radiant as long as you don't remove the appropriate notices from the source code.

    Radiant は MIT ライセンスのもと、ライセンスされている。このことは、Radiant が商用、非営利の利用に対して自由に利用できることを意味する。また、ソースコードからしかるべき条項(著作権表示、許諾表示など)を削除しない限り、Radiant を変更することや、配布することも自由であることを意味する。

Radiant のインストール

gem コマンドで一発だ。


$ sudo gem install radiant

Radiant プロジェクトの作成

radiant コマンドを使用して、Radiant プロジェクトの作成を行う。radiant コマンドの詳細は、ヘルプを参照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ radiant -v
Radiant 0.8.0
$ radiant -h
Usage: /opt/local/bin/radiant /path/to/radiant/app [options]

Options:
    -r, --ruby=path                  Path to the Ruby binary of your choice (otherwise scripts use env, dispatchers current path).
                                     Default: /opt/local/bin/ruby
    -d, --database=name              Preconfigure for selected database (options: mysql, postgresql, sqlite3, sqlserver).
                                     Default: mysql

Radiant Info:
    -v, --version                    Show the Radiant version number and quit.
    -h, --help                       Show this help message and quit.

General Options:
    -p, --pretend                    Run but do not make any changes.
    -f, --force                      Overwrite files that already exist.
    -s, --skip                       Skip files that already exist.
    -q, --quiet                      Suppress normal output.
    -t, --backtrace                  Debugging: show backtrace on errors.
    -c, --svn                        Modify files with subversion. (Note: svn must be in path)

新しい Radiant アプリケーションの作成、そして、初期設定を行う。

以下は、データベースに sqlite3、Radiant アプリケーションの名前として my-app を指定して Radiant アプリケーションの新規作成を行っている。


$ radiant -d sqlite3 my-app

アプリケーションディレクトリに移動して、初期設定を行う。途中何度か入力の必要がある。

1
2
3
4
5
6
7
8
9
10
11
$ cd my-app
$ rake production db:bootstrap
(in /Users/hoge/work/radiant/my-app)
This task will destroy any data in the database. Are you sure you want to 
continue? [yn] y
==  CreateRadiantTables: migrating ============================================
...
Create the admin user (press enter for defaults).
Name (Administrator): 
Username (admin): 
Password (radiant): 

管理者の情報を入力する。() 内がデフォルトの値となる。Name はそのままでよいだろう。UsernamePassword は管理サイトにログインする際に必要になる。

1
2
3
4
5
6
7
8
Initializing configuration.........OK

Select a database template:
1. Empty
2. Roasters (a coffee-themed blog / brochure)
3. Simple Blog
4. Styled Blog
[1-4]: 2

テンプレート(サンプル)となるページを選択する。

1 はその名の通りテンプレートは何もなく全くの白紙からページを作っていくことになる。 これを選択した場合、おそらく管理サイトを見た瞬間、手が止まるだろう。。
初めて Radiant プロジェクトを作成する際には、2 〜 4 のテンプレートを選択し、実際にどのようにコンテンツ管理を行っているのか、参考にするのがよいと思う。

1
2
3
4
5
6
Creating Snippets....OK
Creating Pages....OK
Creating Layouts....OK
Creating Page parts....OK

Finished.

以上で Radiant プロジェクトが作成されている。

起動する。

1
2
3
4
5
$ script/server -e production
=> Booting Mongrel
=> Rails 2.3.2 application starting on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server

http://localhost:3000/ にアクセスしてみる。
以下は、"Select a database template" の選択の際に、" 2. Roasters (a coffee-themed blog / brochure)" を選択した場合のテンプレートになる。

radiant-template-roasters

管理サイトには、http://localhost:3000/admin でアクセスする。

radiant-login

ログイン後の管理サイトのトップページ。

radiant-admin-interface

上記は、Mongrel サーバを使って簡易的に動作させている。

本格的に運用する場合には、Ruby On Rails を稼動させる環境の選択肢は幾つかあるが、Apache のモジュールとして連動させることができる Passenger が設定も比較的容易でお勧めだ。

(蛇足) CMS として、Radiant が日本でなぜブレイクしないのか

個人的には非常によいアプリケーションだと思っているのだが、しつこいが、日本では流行っていないように感じる。。
理由はいろいろあると思うのだが、以下思い付く範囲で。

  1. Ruby On Rails がベースになっている (日本に限った話ではない。。)
  2. 日本語による情報の不足 + 国際化対応の遅れ
  3. Ruby On Rails を十分なパフォーマンスで稼動させる手軽な環境(サービス)の不足
  4. Radiant コアがシンプルすぎる? (日本に限った話ではない。。)
  5. 開発者でない方には少々扱いが難しい? (日本に限った話ではない。。)

「日本」に限った話ではなくなっている。。

Ruby On Rails がベースになっている

「日本」に限った話ではない。

Ruby On Rails は非常によく考えられたフレームワークで、Ruby の世界だけでなく、他言語の世界にもインパクトを与えた。 Ruby On Rails をベースとしていることはメリットではあるのだが、Ruby On Rails を触ったことのない方には敷居になる。

Radiant はある程度の環境を作りあげてしまえば言語の知識など必要なくなるのだが、そのある程度の環境にもっていくまでに多少の Ruby On Rails の知識を要する。
と言っても、それ程専門的な知識ではないのだが、Ruby On Rails を触ったことのない方にとってはちょっとだけ学習コストがかかる。

ちなみに、Radiant にインスパイアされた PHP 版 Radiant である Frog CMS というものもある。

日本語による情報の不足 + 国際化対応の遅れ

のような精力的な活動もあり、最近 Radiant に関する書籍も出版されたようなので、徐々に解消されていくのではないかと思っている。
だが、5つ目の理由を含めて日本語の情報の充実だけでは難しい面もあるように感じている。

国際化対応については、コンテンツの作成に関しては、日本語を使用しても問題ないのだが、管理画面は手を入れないと表示が英語のままとなる。
GitHub のブランチで i18n 対応は進められているが、v0.8.0 のリリースには、残念ながら含まれなかった。

Ruby On Rails を十分なパフォーマンスで稼動させる手軽な環境(サービス)の不足

専用サーバ、VPS 以外で手軽にレンタルサーバを借りて稼動することは難しい。海外だと、

など Ruby On Rails のアプリケーションを十分なパフォーマンスで稼動させることができるサービスがあるのだが、国内では敷居が高い状況が続いている。

Radiant コアがシンプルすぎる?

「日本」に限った話ではない。

それがよい面でもあり、ポリシー的には個人的にはよいと思っているのだが、例えば画像をアップロードしたいと思っても、Radiant コアにはその機能は無い。。
メジャーな CMS では最初から使えるような機能が、エクステンション形式での提供になっている。メジャーな CMS に備わっている機能を全て扱えるようにしようと思った場合に、幾つかのエクステンションを導入するという一手間がかかる。

これは、要件にマッチする、または、良質なエクステンションを自由に選択できるという利点でもあるが、エクステンションの数も半端ではないため、その選択に迷うことになるかもしれない。
自分自身そうだったため、今後エクステンションに関するメモを残しておこうと思っている。

また、この件については、Radiant のメーリングリストにおいても最近また議論がされており、Radiant More という感じで、よく使用されるエクステンションはそれを含めた状態での配布、もしくは、ほとんど手間なく導入できる仕組みが考えられている。

開発者でない方には少々扱いが難しい?

「日本」に限った話ではない。

先にも書いたが、ある程度の環境を整えるまでにエンジニア的な知識が必要となる。
ある程度の環境ができあがっていたとしても、WordPress や MovableType などのようなツールとは若干赴きを異にしていることもあり、「ちょっと更新お願い!」という感じであまり IT 系に詳しくない方に頼むのは難しいかもしれない。

開発者、特に Ruby、Ruby On Rails に触れあっている方にとっては非常に扱い易いものだと思うのだが、運用には一工夫必要かなと思う。

Heroku を利用して新規 Sinatra アプリを5分くらいで公開する

written by taka on July 26th, 2009 @ 02:16 AM  

誇張表現でもなく、サービスの利用開始の手続から公開まで、本当に5分以内でできてしまう。。

Heroku は、"instant ruby platform" と言われるもので、ホスティングサービスではあるが、"準備不要なデプロイ環境" である。 ローカルで作成した Ruby アプリケーション(Rails、Sinatra、Merb etc)をコマンド一発で公開できるプラットフォームを提供する。

そのインターフェイスも素晴らしいが、サービスの公開にあたり、サーバ側のシステム管理作業を行う必要はなく、また、自動的にスケールしてくれる仕組みも提供している点が素晴らしい。

存在自体は以前小耳に挟んでいたのだが、

で触れられていたの読み、試してみることにした。

Heroku は長くベータ版として運用されていたが、2009/04/24 に商用サービスのスタートがアナウンスされた。

商用サービスがスタートした後も、無料で利用可能なサービスを残している。ちょっとしたアプリの公開、ステージング環境、テスト環境などとして利用できる。

ここでは、その公開手順のメモを残す。

Heroku サービスのサインナップ

サインナップを行う。

メールアカウントの入力を行うと、入力したアカウントにメールが送られてくる。

Heroku is a platform for instant deployment of Ruby apps. Develop your app using your local tools, then deploy via Git and the Heroku gem. Follow the link to activate your account:

http://heroku.com/signup/accept/oio2oo2o2o2

To learn more about deploying apps on Heroku, check out the docs:

http://docs.heroku.com

Have fun, and don't hesitate to contact us with your feedback.

  • The Heroku Team http://heroku.com/

指示に従い、http://heroku.com/signup/accept/oio2oo2o2o2 (最後の数値は左記の通りではない) にアクセスする。

開いた画面にて、パスワードの登録を行い、アクティベートする。

Welcome 画面が開き、この後の作業ステップが書かれている。

heroku-startup1 heroku-startup2

これで Heroku サービスの利用が可能となる。

ローカル(開発環境)側の環境準備

gem で heroku コマンドをインストールする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ sudo gem install heroku
Password:
Building native extensions.  This could take a while...
Successfully installed rest-client-1.0.3
Successfully installed configuration-0.0.5
Successfully installed launchy-0.3.3
Successfully installed json-1.1.7
Successfully installed heroku-1.0
5 gems installed
Installing ri documentation for rest-client-1.0.3...
Installing ri documentation for configuration-0.0.5...
Installing ri documentation for launchy-0.3.3...
Installing ri documentation for json-1.1.7...
Installing ri documentation for heroku-1.0...
Installing RDoc documentation for rest-client-1.0.3...
Installing RDoc documentation for configuration-0.0.5...
Installing RDoc documentation for launchy-0.3.3...
Installing RDoc documentation for json-1.1.7...
Installing RDoc documentation for heroku-1.0...

お試しなので、デプロイするアプリは何でもよいのだが、ここでは、GitHub にある Sinatra アプリの Heroku へのデプロイサンプルを使ってみる。

"Congradulations! You're running a Sinatra application on Heroku!" とだけ表示する非常にシンプルなアプリケーションだ。。

1
2
3
4
5
6
7
8
9
$ cd ~/work/
$ git clone git://github.com/sinatra/heroku-sinatra-app dr-heroku-sinatra-test
Initialized empty Git repository in /Users/hoge/work/dr-heroku-sinatra-test/.git/
remote: Counting objects: 38, done.
remote: Compressing objects: 100% (32/32), done.
remote: Total 38 (delta 8), reused 0 (delta 0)
Receiving objects: 100% (38/38), 5.58 KiB, done.
Resolving deltas: 100% (8/8), done.
$ cd dr-heroku-sinatra-test/

アプリケーションを Heroku に配置する

heroku コマンドを使って、ローカルにあるアプリを Heroku にデプロイできる状態にする。

この作業を実施するにあたっては、

  • アカウント作成時に登録を行ったメールアドレスとパスワードの準備
  • SSH で使用する鍵
  • Git の利用環境

上記3点が用意されている必要がある。

heroku create を利用する。引数には、任意のアプリケーション名称を与える。

1
2
3
4
5
6
7
$ heroku create dr-heroku-sinatra-test
Enter your Heroku credentials.
Email: hoge@designrecipe.jp
Password: 
Uploading ssh public key /Users/hoge/.ssh/id_rsa.pub
Created http://dr-heroku-sinatra-test.heroku.com/ | git@heroku.com:dr-heroku-sinatra-test.git
Git remote heroku added

上記コマンドの中で、SSH で利用する公開鍵の登録も行っている。

また、git の設定情報が変更されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git config --list
user.name=Hoge Fuga
user.email=hoge@designrecipe.jp
color.ui=auto
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
remote.origin.url=git://github.com/sinatra/heroku-sinatra-app
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
remote.heroku.url=git@heroku.com:dr-heroku-sinatra-test.git
remote.heroku.fetch=+refs/heads/*:refs/remotes/heroku/*

remote の情報が追加されているようだ。

Heroku の環境に配置する。と言っても、通常の Git コマンドを利用して push しているだけだ。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git push heroku master
The authenticity of host 'heroku.com (75.101.145.87)' can't be established.
RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'heroku.com,75.101.145.87' (RSA) to the list of known hosts.
Counting objects: 38, done.
Compressing objects: 100% (24/24), done.
Writing objects: 100% (38/38), 5.72 KiB, done.
Total 38 (delta 8), reused 38 (delta 8)

-----> Heroku receiving push
-----> Sinatra app detected
   Compiled slug size is 4K

-----> Launching...... done
   http://dr-heroku-sinatra-test.heroku.com deployed to Heroku

To git@heroku.com:dr-heroku-sinatra-test.git
 * [new branch]  master -> master

以上で完了となる。

デフォルトでは、heroku.com のサブドメインとして、<app_name>.heroku.comで公開される。 <app_name> は、heroku create を使用した際に命名したアプリケーション名になる。
上記手順の場合、http://dr-heroku-sinatra-test.heroku.com/ になる。

heroku-first-test-site

当然ながら、自ドメインも利用することは可能。

参考サイト

Sinatra ベースの軽量アプリいろいろ Scanty 編

written by taka on July 25th, 2009 @ 02:04 AM  

Sinatra ベースのアプリを調べており、その一環として、あちこちで名前を聞く手頃なアプリを見てみることにする。

とってもシンプルなブログツール Scanty

「スキャンティ」と片仮名で書いてしまうと、若干いやらしくも響くが、とてもシンプルな Sinatra ベースのブログエンジンだ。

作者本人もこのツールを利用してブログを書いているようだ。

と・・・この作者の方って、Heroku の人なんだ。。

Scanty の特徴としてはサイトの記載から拾ってみると、以下のような内容になっている。

特徴

  • ブログ"エンジン"とまでは言えないけれど、コンパクトでカスタマイズを入れるのは容易。
    ブログツールの基礎として使える。
  • ポストできる!(特徴かな。。)
  • タグ付け
  • 入力に使用できる記法: Markdown
  • コメント機能: Disqus
  • Atom feed

作者も書いているように、これを土台に自分色に染めてね(この通りには作者は言っていない)、というものである。

コメント機能に関しは、ツールの中に抱えないで、DISQUS を使用するようにしている。

原材料

  • Web フレームワーク: Sinatra
  • Markdown -> HTML への変換: Maruku
  • コードシンタックスハイライト(Only Ruby): Syntax
  • ORM: Sequel

ライセンスは、MIT License だ。

軽く動作を見てみて、ちょっと気になるところには手を入れてみることにする。

インストールと設定、そして起動

Sinatra がもしインストールされていないのであれば先に Sinatra を入れておく。


$ gem sudo install sinatra

Scanty を GitHub から clone し、自分用のブランチを用意しておく。

1
2
3
4
5
6
7
$ cd ~/work
$ git clone git://github.com/adamwiggins/scanty.git
$ cd scanty
$ git checkout -b taka-blog
$ git branch
  master
* taka-blog

main.rb に書いてあるデフォルトの設定を変更しておく。

1
2
3
4
5
6
7
8
9
Blog = OpenStruct.new(
       :title => 'a scanty blog',
       :author => 'John Doe',
       :url_base => 'http://localhost:4567/',
       :admin_password => 'changeme',
       :admin_cookie_key => 'scanty_admin',
       :admin_cookie_value => '51d6d976913ace58',
       :disqus_shortname => nil
)

とりあえずローカルでの動作確認レベルであればこのままでもよいが、外部公開するのであれば、

  • :admin_password
  • :admin_cookie_key
  • :admin_cookie_value

の変更が必須だろう。cookie value はランダムな値で。

起動する。


$ ruby main.rb

先に設定した main.rb:usr_base のアドレスにアクセスする。

また、rake による起動も可能。

1
2
3
4
$ rake -T
(in /Users/taka/work/sinatra/scanty)
rake start  # Start the app server
rake stop   # Stop the app server

Rakefilestartstop タスクが定義してある。port 番号の定義がデフォルトで 3030 になっているので、適切なポート番号に変更。

投稿してみる

サーバを起動し、初めてアクセスを行うと以下の画面が表示される。

scanty-first-access

まずは、"Log in" をクリックしログインする。

(次回以降のログインの際には、/auth のパスを直打ちする)

scanty-admin-login

パスワードが合っていれば、最初に開いた画面に戻るので、今度は、"create a post" をクリックする。

scanty-post

投稿画面は非常にシンプル。項目のラベルが何もないが、上から、タイトル、タグ、本文の入力項目となっている。
タグはスペース区切りで複数指定できるようになっている。

"save" ボタンで投稿する。

scanty-posted

続けて投稿してみて、トップページを見てみる。

scanty-blog-list

2回目の投稿に、Ruby のソースを含めてみたのだが、少々表示がおかしいようだ。 また、入力の必須チェックなどが全く入っていない、日本語を含んだタイトルはまともに扱えない、というところがあるので、手を入れてみる。

オリジナルに変更を入れておく

起動してちょっと見ていただけで、とりあえず手を入れておきたいところが数点あった。手始めに以下の3点の変更を入れておくことにする。

  • 入力項目の必須チェック
  • タイトルで日本語の利用を可能に
  • ソースコードの表示部分の変更

入力項目の必須チェックぐらいは入れておく

全く入っていないので入れておく。

この Scanty は、ORM に Sequel を使用している。Sequel は初体験で全く知識がない。ActiveRecord に似てるところもあるので、今回は深く調査せず、勘だけで書いてみる。。

検証ロジックは、ActiveRecord 同様、Model の validate メソッドに入れておいて、エラーのコレクションを追加してやればよさそうだ。 検証失敗時のハンドルは、Sequel::Model#save 時の validation チェックにが raise する Sequel::ValidationFailed の例外でハンドルしようと思ったのだが、Sequel::ValidationFailed がないと言われる。。

Scanty は、Sequel を vendor ディレクトリ配下に持っているのだが、取得したリポジトリに含まれているものはバージョン的に古そうだ。gem で最新のものをインストールし、そちらを使うようにする。自前で持っている vendor 配下への load path の追加は消しておく。

1
2
3
4
5
$ gem list sequel

*** LOCAL GEMS ***

sequel (3.2.0)

サーバの再起動をかけると、

1
2
$ ruby main.rb 
./lib/post.rb:7: undefined method `table_exists?' for Post:Class (NoMethodError)

と怒られる。 新しいバージョンではお作法が変わったようだ。Sequel::Model.plugin(:schema)main.rbconfigure メソッドのコードブロックに追加しておく。

main.rb:

1
2
3
configure do
  Sequel::Model.plugin(:schema)  # 追加
  Sequel.connect(ENV['DATABASE_URL'] || 'sqlite://blog.db')

(なんとも手探りな状態だ。。)

気を取り直して Model にバリデーションを入れておく。

lib/post.rb:

1
2
3
4
5
6
7
class Post < Sequel::Model
...
  # validate メソッドの追加
  def validate
    errors[:title] << "can't be empty" if title.empty?
    errors[:body] << "can't be empty" if body.empty?
  end

そのハンドリングは、

main.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
post '/posts' do
  auth
  post = Post.new(
    :title => params[:title],
    :tags => params[:tags],
    :body => params[:body],
    :created_at => Time.now,
    )

  # begin - resucue - end を追加
  begin
    post.save
    redirect post.url
  rescue Sequel::ValidationFailed
    erb :edit, :locals => { :post => post, :url => '/posts' }
  end
end

としておく。

エラーがあった時の表示だが、さすがに、ActionView::Helpers::ActiveRecordHelper#error_messages_for のようなものは無い。。

適当に作っておく。

main.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
helpers do
  ...
  # なんちゃって error_message_for を追加
  def error_message_for(model)
    unless model.errors.size == 0
      html = ''
      html << '<ul>'
      model.errors.each do |k, v|
        html << "<li>#{k.to_s.capitalize} #{v.first}.</li>\n"
      end
      html << '</ul>'
    else
      ''
    end
  end
end

view に追加しておく。

lib/edit.erb:

1
2
<%# error_message_for の呼び出しを追加 %>
<%= error_message_for(post) %>

一応、これで必須チェックは入るようになる。

scanty-error-check1

タイトルで日本語の表示を可能に

最近の舶来物の場合、permalink の生成に以下のようなロジックを入れてくる。

lib/post.rb:

1
2
3
  def self.make_slug(title)
    title.downcase.gsub(/ /, '_').gsub(/[^a-z0-9_]/, '').squeeze('_')
  end

これだと、マルチバイト文字は空文字になってしまう。。それがタイトルに日本語を使えない理由。

URL エンコードするのは嫌なので、対応としては、タイトルはタイトルとして入力値をそのまま残し、permalink を構成する slug 部分は、投稿者に自分で入れてもらうことにする。

  1. view に入力用の slug フィールドの追加
  2. Post インスタンス生成時のパラメータに直接 slug フィールドの入力項目を設定
  3. slug フィールドも必須チェック対象に

とすることにする。

1つ目の対応。

lib/edit.erb:

1
2
3
4
<div class="postzoom">
  <h2 class="title"><input type="text" name="title" value="<%= post.title %>" /></h2>
  <p class="meta"><input type="text" name="slug" value="<%= post.slug %>" /></p> <!-- 追加 -->
  <p class="meta"><input type="text" name="tags" value="<%= post.tags %>" /></p>

2つ目の対応。

main.rb:

1
2
3
4
5
6
7
8
9
post '/posts' do
  auth
  post = Post.new(
    :title => params[:title],
    :tags => params[:tags],
    :body => params[:body],
    :created_at => Time.now,
    :slug => params[:slug] # 追加
    )

3つ目の対応。

lib/post.rb:

1
2
3
4
5
6
7
class Post < Sequel::Model
  ...
  def validate
    errors[:title] << "can't be empty" if title.empty?
    errors[:slug] << "can't be empty" if slug.empty? # 追加
    errors[:body] << "can't be empty" if body.empty?
  end

以上で OK。

投稿画面にラベルが無いと流石にわかり難くなってきたので、ラベルをついでにつけておく。

scanty-edit-view

ソースコードの表示部分の変更

投稿された本文は、Markdown のパーサ(Maruku) を通って HTML 化され、さらに Syntax を通ってコードハイライト用のタグが補足される。

ソースの該当箇所は、以下のようになっている。

lib/post.rb:

1
2
3
4
5
6
7
8
9
10
11
class Post < Sequel::Model
....
  def to_html(markdown)
    h = Maruku.new(markdown).to_html
    h.gsub(/<code>([^<]+)<\/code>/m) do
      convertor = Syntax::Convertors::HTML.for_syntax "ruby"
      highlighted = convertor.convert($1)
      "<code>#{highlighted}</code>"
    end
  end
....

で、表示は以下の通り。

scanty-codeview

まず、ソースコードの枠の部分が2重になっているのは、<pre>...</pre> の中に、<code><pre>...</pre><pre>...</pre></code><pre> タグが入れ子になってしまうからのようだ。作者のブログのページ を見てみたが、そうはなっていない。HTML の表記として何が正しいのかをキチンと確認する必要もあるが、

  • <pre> が入れ子になっている
  • ソースコード部分での実体参照への勝手な置き換えが行なわれている

という点に修正を入れないと表示がおかしくなってしまう。

まずは現在のソースでどのように動作するか、状況を把握する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ irb
>> require 'rubygems'
=> true
>> require 'maruku'
=> true
>> require 'syntax/convertors/html'
=> true
>> str = File.read('t.md')
=> "second!\n\nclass Hello\n   def hello\n puts 'hello!'\n   end\nend\n"
>> org_reg = Regexp.new('<code>([^<]+)</code>', Regexp::MULTILINE)
=> code[]codem
>> org_syn_convert_proc = lambda { "<code>#{Syntax::Convertors::HTML.for_syntax('ruby').convert($1)}</code>" }
=> #<Proc:0x0110169c@(irb):8>
>> org_h = Maruku.new(str).to_html
=> "<p>second!</p>\n\n
<pre><code>class Hello\n   def hello\n puts &#39;hello!&#39;\n   end\nend</code></pre>"
>> org_h.gsub(org_reg, &org_syn_convert_proc)
=> "<p>second!</p>\n\n
    <pre>
      <code>
        <pre>
          <span class="keyword">class </span>
          <span class="class">Hello</span>\n
          <span class="keyword">def </span>
          <span class="method">hello</span>\n
          <span class="ident">puts</span>
          <span class="punct">&amp;</span>
          <span class="comment">#39;hello!&amp;#39;</span>\n
          <span class="keyword">end</span>\n
          <span class="keyword">end</span>
        </pre>
      </code>
    </pre>"

確かに問題となっている状況の通りだ。

この状況に対して、

  • シンタックスハイライトを適用させるための Convert 処理を変更する
    • <pre> が入れ子になっている件への対応
  • Markdown から HTML への変換には、Maruku の代わりに、BlueCloth を使う
    • ソースコード部分での実体参照への勝手な置き換えが行なわれている件への対応

の2点を行ってみた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>> require 'bluecloth'
=> true
>> changed_reg = Regexp.new('<pre><code>([^<]+)</code></pre>', Regexp::MULTILINE)
=> precode[]codeprem
>> changed_syn_convert_proc = lambda { Syntax::Convertors::HTML.for_syntax('ruby').convert($1) }
=> #<Proc:0x0108f27c@(irb):15>
>> b_h = BlueCloth.new(str).to_html
=> "<p>second!</p>\n\n<pre><code>class Hello\n   def hello\n puts 'hello!'\n   end\nend\n</code></pre>"
>> b_h.gsub(changed_reg, &changed_syn_convert_proc)
=> "<p>second!</p>\n\n
    <pre>
      <span class="keyword">class </span>
      <span class="class">Hello</span>\n
      <span class="keyword">def </span>
      <span class="method">hello</span>\n
      <span class="ident">puts</span>
      <span class="punct">'</span>
      <span class="string">hello!</span>
      <span class="punct">'</span>\n
      <span class="keyword">end</span>\n
      <span class="keyword">end</span>\n
    </pre>"

一応、これで表示上は問題なくなる。
(後で、もう少しちゃんと考えよう。)

scanty-codeview-fixed

最終的な Post#to_html の変更箇所は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git diff
diff --git a/lib/post.rb b/lib/post.rb
index 3acb149..9f5c20b 100644
--- a/lib/post.rb
+++ b/lib/post.rb
@@ -1,4 +1,4 @@
-require File.dirname(__FILE__) + '/../vendor/maruku/maruku'
+require 'bluecloth'
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../vendor/syntax' require 'syntax/convertors/html'
@@ -56,11 +56,10 @@ class Post < Sequel::Model
######## def to_html(markdown)
- h = Maruku.new(markdown).to_html
- h.gsub(/<code>([^<]+)<\/code>/m) do
+ h = BlueCloth.new(markdown).to_html
+ h.gsub(/<pre><code>([^<]+)<\/code><\/pre>/m) do
convertor = Syntax::Convertors::HTML.for_syntax "ruby"
- highlighted = convertor.convert($1)
- "<code>#{highlighted}</code>"
+ convertor.convert($1)
end end

感想

  • 機能が最小限ということもあるが、Sinatra ベースのアプリは非常にシンプルでわかり易い。
  • 「HTTP メソッド + パス」がダイレクトに処理コードに紐付くのは気持ちいい。
  • Sequel って結構いいかも。

Sinatra の作者が言うように Sinatra はフレームワークというよりは、ライブラリ的なものだ。 Rack に対して非常にセンスのよいデコレーションを行っている。

Rails はフルセットのフレームワークであり、非常に有用で、その設計は勉強にもなる。 ただ、その内部は複雑化しており、何か問題のあった時などには知っておくべきことが多すぎて、思わぬところで時間をとられる場合がある。

適材適所で、シンプルで扱い易い Sinatra を使ってみたいと思えてくる。