gonzuiをrackupしてみた
気になりながらもちゃんと中までは見ていなかったRackを触ってみることにした。「Hello, World!」的なコードはすでにあるので何かをRackに載せてみようと思い、最近身のまわりでちょっとしたブームであるgonzuiを選んだ。Rackのバージョンは0.4.0。
gonzuiに含まれるwebインタフェースであるgonzui-serverはWEBrickを使って書かれている。検索エンジンとwebインタフェースの間には中間層のようなものはないようなので、効率は悪くなりそうだがwrapperを入れることにした。最悪のケースだと[gonzui-engine]<==>[gonzui-server]<--(webrick api)=(rack spec)-->[rack]=[webrick handler]<==>[webrick]のようになる。その代わりに足まわりを変えられる利点が得られる。
とまどったところはいくつかある。
一つはURLMapの使い方で、コードを書き足しながらいろいろ試していたため、ある時点でこんな状態になった。
map "/foo" do run App_foo.new end run App.new
これはrackup用のスクリプトで、最初はrun App.newだったところにmapを足したもの。意図は/fooへのアクセスをApp_fooで処理させるコードを足したつもりだった。結果は以下のようになる。
$ rackup test.ru
.../gems/rack-0.4.0/lib/rack/builder.rb:49:in `to_app': undefined method `call' for {"/foo"=>#}:Hash (NoMethodError)
mapはアプリケーションのリストの最後にハッシュを入れて、そこでURLと振り先のアプリケーションとの対応付けを作る。一方runはアプリケーションのリストの最後に入れる。rackupはこのリストに対してto_appを送り、内部のオブジェクト構造を作る。その際、リストの最後にあるハッシュについてだけは特別な扱いをしていて、URLMapオブジェクト(アプリケーションと呼ぶべき?)に変換する。しかし中間にあるハッシュについてはケアされないため上のような例外が発生する。正しくはAppに対してもmapを使う。
map "/foo" do run App_foo.new end map "/" do run App.new end
もう一つのとまどいはRack::CommonLoggerの動作で、ちょうど上のコードを与えたときにログに記述録されるアクセス先のURLが空になってしまうことがある。
$ rackup test.ru [2008-09-19 11:36:25] INFO WEBrick 1.3.1 [2008-09-19 11:36:25] INFO ruby 1.8.7 (2008-08-11) [i486-linux] [2008-09-19 11:36:30] INFO WEBrick::HTTPServer#start: pid=25843 port=9292 127.0.0.1 - - [19/Sep/2008 11:36:33] "GET / HTTP/1.1" 200 3 0.0007 127.0.0.1 - - [19/Sep/2008 11:36:35] "GET HTTP/1.1" 200 7 0.0007
具体的には/以外のマップへのアクセスの際のログについてベース部分、つまりmapの引数に指定した部分がログの上では消えてしまう。アプリケーションはきちんと動くのですごく困るわけではなかったのだけど、Rackのコードを見るとこの部分(PATH_INFOの内容が出力されている)を意図して削っているため悩んでしまった。結果からいうとRack::Recursiveを使えばこの問題を回避できるようだ。Rack::Recursiveは内部回送をする際に使うものなので、今回は必要ないと思うのだが、このようなわけで入れておくことにした。
ところで、このPATH_INFOを削っているのが、実は、よくできているところの一つだと後で気付いた。というのは上のコードをCGIスクリプトとして実行した場合などでもCGIのPATH_INFOを見て調整してくれるようで、たとえば/cgi-bin/test.ru/fooにアクセスするとちゃんとApp_fooが応答する。
ちょっと困ったのはgonzui-serverが生成するリンクが絶対パスになっていること。gonzuircの:base_mount_pointの値(デフォルトは/)を使って生成されている。Rackにgonzuiを載せるときにもこの値を参照しておいたほうが良さそうなのだけど、WEBrickやMongrelとCGIやFastCGIで、このあたりの関係がややこしいことになる。
たとえば:base_mount_pointを/codesearchとしたとする。導かれる絶対パスは/codesearchとなり、下位のサーブレットへのパスは/codesearch/statなどとなる。このときWEBrickなどでは絶対パスでURLMapを作っておけば特に問題はない。CGIなどでは一般に/cgi-bin/gonzui.cgiがアプリケーションのトップになり、下位サーブレットへは/cgi-bin/gonzui.cgi/statなどでアクセスさせることになる。この場合、HTTPサーバのエイリアス機能によって/cgi-bin/gonzui.cgiを/codesearchに対応付けておかなければならないものの、ひとまずは問題ない。
ここでpublic_htmlに置くことを考える。こういう状況では前述のようなエイリアスを設定するのは難しいことが少なくない。この場合、:base_mount_pointを設定しないでおけばよさそうに思えるのだが、デフォルト値が/であるため/~foo/gonzui.cgiによって生成されるリンクがたとえば/statなどとなってしまう。では:base_mount_pointを/~foo/gonzui.cgiとするとどうか、というと、今度はURLMap側で問題が起きる。前述のように絶対パスでURLMapを作っている場合、この指定は/~foo/gonzui.cgi/~foo/gonzui.cgiがトップであることになってしまう。となるとURLMapを作るときに:base_mount_pointを参照しなければ良さそうなのだが、そうすると今度はWEBrickやMongrelでしぶいことになる。
そんなようなわけで、しょうがないのでadhocに:base_mount_pointを変更して動作するようなwrapperにした。
最後に、CGIやFastCGIでのRackの運用方法がもう一つよくわからなかった。FastCGIについては外部プロセスとして起動してしまうということはもちろん考えられるのだが、動的運用ではどうすると良いのか。rackupを実行するシェルスクリプトを置いておけば動くことは動く。でもなんとなく釈然としない。検索などしてもうまくヒットさせられなかったので、ひとまずはgist:28033のような構成とした。
gonzui-rack.rbの中ではRack::Builderを使ってmapなどしている。Rack::Builderはrackupの中でも使われているが、これは(他のRackミドルウェアと同様に)ネストできるので特に問題ないようだ。
ちなみにabでちょっとだけ比較してみたところ、こんな感じだった。まあ、あまり意味はないかも。
type conc Reqs per Sec Time per Req Time per Req (all) ------------ ---- ------------ ------------ ------------------ WEBrick 1 52.71 18.972[ms] 18.972[ms] WEBrick 3 46.95 63.902[ms] 21.301[ms] Rack/WEBrick 1 54.13 18.475[ms] 18.475[ms] Rack/WEBrick 3 45.85 65.425[ms] 21.808[ms] Rack/CGI 1 3.43 291.723[ms] 291.723[ms] Rack/CGI 3 6.37 470.852[ms] 156.951[ms] Rack/FCGI 1 50.93 19.635[ms] 19.635[ms] Rack/FCGI 3 95.80 31.316[ms] 10.439[ms]
URI::Parser
URIモジュールをいじくって、RFC的にはダメなんだけどちょっとだけ大目に見てよ、というようなことが出来るようにしてみた。というのは、その種のリクエストを何度か受け取ったことがあるため。リクエストというよりも、バグ報告として受け取ることが多かったと思うけど。
>> p = URI::Parser.new(:ESCAPED=>"(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
=> #<URI::Parser:0xb7872640>
>> u = p.parse("http://foo.bar.baz/%uABCD")
=> #<URI::HTTP:0xb78cf4f8 URL:http://foo.bar.baz/%uABCD>
>> URI.parse(u.to_s)
URI::InvalidURIError: bad URI(is not URI?): http://foo.bar.baz/%uABCD
from /.../lib/uri/common.rb:126:in `split'
from /.../lib/uri/common.rb:144:in `parse'
from /.../lib/uri/common.rb:592:in `parse'
from (irb):3
from /usr/bin/irb1.9:12:in `<main>'
>> s = "http://foo.bar.baz/ABCD"
=> "http://foo.bar.baz/ABCD"
>> u1 = p.parse(s)
=> #<URI::HTTP:0xb78c3220 URL:http://foo.bar.baz/ABCD>
>> u2 = URI.parse(s)
=> #<URI::HTTP:0xb78b6d54 URL:http://foo.bar.baz/ABCD>
>> u1 == u2
=> true
>> u1.eql?(u2)
=> false
あとでruby-devに投げてみよう。
Ruby/GD2でGIFアニメ
用事があってGIFのフォーマットだとかGIFアニメのことだとかを調べている。特許の話とかでてきて懐かしい。
GIFアニメについてはlibgd2の2.0.29で対応したようだ。が、言語バインディングではまだあまり対応していない様子。まだ、というか、libgd2の言語バインディングはあまりアクティブでないような感じもある。Ruby/GDもそんななかの一つ。今からコードを足すのもどうかなというところ。
そう思っていたらRuby/GD2という別の実装があることに気付いた。こちらもアクティブではないようだが、Ruby/DLで書かれているためちょっとだけ手を入れやすい。そういうわけで、関数をいくつか足し、Rubyレベルでのメソッドを三つほど足してGIFアニメを出力できるようにしてみた(差分)。
require 'gd2'
files = Dir.glob('/path/to/images/*.png')
prev = nil
open('/path/to/output.gif', 'w') do |io|
img = nil
files.each do |f|
img = GD2::Image.import(f, :format => :png)
img.gifanim_begin(io) if prev.nil?
img.gifanim_add(io, 50, prev, true)
prev = img
end
img.gifanim_end(io)
end
使っているlibgd2は2.0.36-RC1なのだけど、このバージョンのGIFアニメ機能にはバグがあるようだ。libgd2では、GIFアニメを構成するイメージの間での変更部分を抽出しようとするのだが、サイズが異なるイメージが与えられると、その処理の中でSEGVを起こす。ソース中のコメントには内部で調整するというようなことになっているのに、実際にはそういうことをやっているところがない。そのため、前述したメソッドでは前フレームのサイズに変換してしまうことにした。ま、そろえておけよっていうことではあるのだろうと思うのだけど。
追記(2008-10-04): 渡辺哲也さんの日記を見てパッチを置いていなかったことに(今になって)気付いた。それとも消してしまったんだったかな。
Rails 2.0.2とRuby 1.8.7のString#respond_to?
ところが(なのかな?)Rails 2.0.2にも同じバグがあって、これも顕在化した。よって、次のようになるのは1.8.7を1.8.7-p17にしても変わりない。
$ ./script/console
Loading development environment (Rails 2.0.2)
>> "" + ActiveSupport::Multibyte::Chars.new("")
ArgumentError: wrong number of arguments (2 for 1)
from (irb):1:in `respond_to?'
from (irb):1:in `+'
from (irb):1
>>
つまりこれはActiveSupport::Multibyte::Charsで定義されているrespond_to?が引数を一つしか取らないからで、これを二つ取るようにすればよい。というわけで、config/initializers/fix_multibyte_chars_respond_to.rbというようなファイルを以下の内容で作ることで回避できるような気がする。
if defined?(ActiveSupport) &&
defined?(ActiveSupport::Multibyte) &&
defined?(ActiveSupport::Multibyte::Chars)
mc = ActiveSupport::Multibyte::Chars.new("")
begin
"" + mc
rescue ArgumentError
raise unless mc.method(:respond_to?).arity == 1
class ActiveSupport::Multibyte::Chars
def respond_to?(method, ip = false)
super || @string.respond_to?(method, ip) || handler.respond_to?(method, ip) ||
(method.to_s =~ /(.*)!/ && handler.respond_to?($1, ip)) || false
end
end
end
end
Ruby Enterprise Edition 1.8.6-20080507をbuildしてみた
Ruby Enterprise Editionをbuildしてみた。
tarballをとってきて、展開して、installスクリプトを実行すれば良い、はずなのだが、手元の環境ではうまくいかなかった。どうやら(環境によって)google-perftools-0.97に含まれるlibtcmallocを使おうとして、先にインストールし、それを参照させるべくccにインクルードパスやライブラリパスを伝えようとしているようだ。ライブラリパスについてはRuby本体のbuildのときに-Lなどを使って指定しているのだが、インクルードパスにいついては環境変数C_INCLUDE_PATHやCPLUS_INCLUDE_PATHに値を設定することで指定しようとしている。で、このC_INCLUDE_PATHなどへの設定内容がどうもまずい。実環境に合っていないと必要なヘッダファイルなどを見付けられないことになる。
/bin/sh ./libtool --tag=CXX --mode=compile g++ -DHAVE_CONFIG_H -I. -I. \
-I./src -I./src -pthread -DNDEBUG -Wall -Wwrite-strings \
-Woverloaded-virtual -Wno-sign-compare -g -O2 -c -o \
libtcmalloc_minimal_la-system-alloc.lo `test -f 'src/system-alloc.cc' || \
echo './'`src/system-alloc.cc g++ -DHAVE_CONFIG_H -I. -I. -I./src -I./src \
-pthread -DNDEBUG -Wall -Wwrite-strings -Woverloaded-virtual \
-Wno-sign-compare -g -O2 -c src/system-alloc.cc -fPIC -DPIC -o \
.libs/libtcmalloc_minimal_la-system-alloc.o
In file included from /usr/include/c++/4.3/bits/postypes.h:47,
from /usr/include/c++/4.3/bits/char_traits.h:47,
from /usr/include/c++/4.3/string:47,
from ./src/base/commandlineflags.h:52,
from src/base/logging.h:48,
from src/system-alloc.cc:51:
/usr/include/c++/4.3/cwchar:52:24: error: wchar.h: No such file or directory
なんだ? と思って問題が起きているsource/vendor/google-perftools-0.97でmakeをたたくと処理がもう少し進んでしまったりもするのだが、実はconfigure中に同じエラーが起きているため、しばらくするとまたコケる。
installスクリプトの中でやっていることをよく見てみると、インクルードパスを指定する必要はどうもなさそうに思える。というわけで、以下の変更を加えてinstallスクリプトを動かしてみたところbuildが異常終了しなくなった。
--- ruby-enterprise-1.8.6-20080507.orig/installer.rb 2008-05-26 17:51:36.000000000 +0900
+++ ruby-enterprise-1.8.6-20080507/installer.rb 2008-06-12 12:06:48.000000000 +0900
@@ -123,8 +123,10 @@
File.open("source/.prefix.txt", "w") do |f|
f.write(@prefix)
end
+=begin
ENV['C_INCLUDE_PATH'] = "#{@prefix}/include:/usr/include:/usr/local/include:#{ENV['C_INCLUDE_PATH']}"
ENV['CPLUS_INCLUDE_PATH'] = "#{@prefix}/include:/usr/include:/usr/local/include:#{ENV['CPLUS_INCLUDE_PATH']}"
+=end
ENV['LD_LIBRARY_PATH'] = "#{@prefix}/lib:#{ENV['LD_LIBRARY_PATH']}"
end
installスクリプトによらず、source/vendor/google-perftools-0.97でconfigure --prefix=<インストールパス> --disable-dependency-trackingを実行の上、make libtcmalloc_minimal.la && mkdir -p <インストールパス>/lib && cp .libs/libtcmalloc_minimal.so* <インストールパス>/lib/としておくというのでも回避できるだろうと思う(その後でinstallスクリプトを実行する)。
これでPassengerを動かしてみたいところだが、また後で。
$ ./ruby -v ruby 1.8.6 (2008-03-03 patchlevel 114) [i686-linux] $ ./ruby -e 'p GC.copy_on_write_friendly?' true
Rails 2.0.2とRuby 1.8.7のString#chars
Ruby 1.8.7からはString#charsが定義されるようになった。これがRails 2.0.2なんかではまずくって、たとえば"hello".firstでNoMethodError: undefined method `[]' for #<Enumerable::Enumerator:0xb6f3f5b4>などとなる。これはRailsのように積極的に組み込みクラスを改変しているものの定めといえる。
Rails 2.1.0を見てみると以下のようになっていて、Ruby 1.8.7のString#charsを削除し、Rails(ActiveSupport)が定義しているcharsを使おうとしている。
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
unless '1.9'.respond_to?(:force_encoding)
# Define methods for handling unicode data.
module Unicode
def self.append_features(base)
if '1.8.7'.respond_to?(:chars)
base.class_eval { remove_method :chars }
end
super
end
[...]
Rails 2.0.2でも同じようにするには、Rails側に手を入れたほうがてっとり早いとは思うのだが、ひとまずそれはしないことにするならばconfig/initializers/remove_string_chars.rbを以下の内容で作っておくというやり方はどうだろう。
unless '1.9'.respond_to?(:force_encoding)
String.class_eval do
begin
remove_method :chars
rescue NameError
# OK
end
end
end
script/consoleやscript/server(アプリケーションはTracks-1.6)で簡単な動作確認をしてみたところ、ひとまず前述の例外を回避できているようだ(Ruby 1.8.7、1.8.7-p17および1.8.6で確認した)。ただしそれ以外の環境やアプリケーションでどうなるかは確認していない。
Ruby Way第二版
とある事情により、kmutoさん&トップスタジオさんから
Ruby Way第二版[rakuten](cbook24)の献本をいただいた。事情…… 率直に言うと、出版されているのを知ったときにはびっくりした。まあ、イロイロあるんだなって。
でも、それはそれ、これはこれ。以下、本の内容について。
最初の印象としては読者象を想像し難しいかなというところ。目次を見てピンとくる人はもちろん手にとってみても良いと思う。あとはいろいろなRuby表現を解説付きで学びたいといった要求にも応えられるのかな。そういってみると、入門書を終えた人や実用を始めたばかりの人なんかにおすすめできるのかもしれない。ちなみに本書の目次はSEshop.comでは見られないようなのだが、cbook24の商品ページで参照できる。
ちょっと気になるのは監訳注のバラつき。たとえば:
- 「このコードはRuby 1.8.6および1.9.0で問題なく動作します」というような、意味のとりにくいものがある(では監注が付いていないコードはどうなのか、と気になる)
- 「最近のバージョンでは」のような表現があるが、それは1.9.0-0のことなのか1.8.5とか1.8.6のことなのか
- 本文から少し距離のある(原著者がいれるような内容の)ものがあったりする
もちろん理解を助けてくれる監訳注もあるので、もう少しだけ全体的なバランスがとれているとより読み易くなったのではないかなと思う。
ついでにいうと、このタイミングで1.9系の情報に手を出すのは難しいのではなかったか、とも思う。構成上、どうしても断片的になってしまって網羅性とれないので、それはRuby 1.9 Wayになのかというと、ちょっと違うのではないかな、と。この本の主旨はそういうところに置かれているわけなので、1.9.0-0と1.9.0-1と、さらにその後でけっこうな変化がある状況の中、あえて入れないことを選んでも良かったのではないかしら。
(ふとRuby Way 1の監訳注を読み返してみたりして。視点の違いがちょっと面白い。)
本の発売日をGoogleカレンダーで見られるようにする
以前、Ruby/Amazonを使って書いたスクリプトをamazon-ecs 0.5.1で書き換えた。ついでにiCalendarを1.0.2に更新した。
amazon-ecsは、Ruby/Amazonのような抽象化はしてくれない。パラメータからREST的URLを構成するのと、レスポンスからHprictoのDocオブジェクトを生成するのを助けてくれる程度でしかない。いくつかのメソッドを追加提供してくれてもいるのだが、網羅的ではないので使い分けに困るようなところもある。よって、スクリプトとしてはHpricotによるXMLドキュメントの操作を繰り返すような内容になりがちである。
require 'amazon/ecs'
access_key_id = 'アクセスキーID'
search_keys = YAML.load_file('words.yml') # {type => [検索語, ...], ...}というハッシュ
date_limit = Date.today - 7
Amazon::Ecs.options = { # グローバルなオプション設定
:AWS_access_key_id => access_key_id,
:country => :jp,
:response_group => ['ItemAttributes', 'Images'],
}
# イメージ情報を取り出す
def get_image_info(doc, tag)
tmp = doc/tag
return nil unless tmp
url = tmp/'url'
height = tmp/'height'
width = tmp/'width'
return nil unless url && height && width
{:url =>url.first.inner_text,
:height => height.first.inner_text.to_i,
:width => width.first.inner_text.to_i}
end
# 検索実行
def ecs_search(word, config, date_limit)
products = []
opts = config[:options].dup
page = 1
loop do
opts[:item_page] = page # ページめくりのため
res = Amazon::Ecs.item_search(word, opts)
if res.has_error?
(res.doc/'errors/error').each do |e|
code = (e/'code').inner_text
if code == 'AWS.ECommerceService.NoExactMatches'
# not found
else
raise "#{code}: #{(e/'message').inner_text}"
end
end
end
break if res.total_pages == 0
res.items.each do |item|
prod = {}
attrs = item.search_and_convert('itemattributes')
prod[:url] = item.get('detailpageurl')
prod[:asin] = item.get('asin')
prod[:title] = attrs.get('title')
prod[:small_image] = get_image_info(item, 'smallimage')
prod[:medium_image] = get_image_info(item, 'mediumimage')
prod[:large_image] = get_image_info(item, 'largeimage')
rel_date = attrs.get(config[:date_field])
next if rel_date.nil?
case rel_date
when /^\d{4}$/
if rel_date.to_i < date_limit.year
# date_limitより古くなったら中断
page = res.total_pages
break
end
next
when /^\d{4}-\d{2}$/
rel_date << '-28' if /^\d{4}-\d{2}$/ =~ rel_date
when /^\d{4}-\d{2}-\d{2}$/
# noop
else
next
end
prod[:release] = Date.parse(rel_date)
if prod[:release] < date_limit
# date_limitより古くなったら中断
page = res.total_pages
break
end
products << prod
end
break if products.empty?
break if res.total_pages == page
page += 1
end
products
end
products = {}
{ # 検索対象とその条件
'author' => {
:options => {
:search_index => 'Books',
:type => :author,
:sort => 'daterank', # 日付順ソートを指定する
},
:date_field => 'publicationdate',
},
# (略)
'actor' => {
:options => {
:search_index => 'DVD',
:type => :actor,
:sort => '-releasedate',
},
:date_field => 'releasedate',
},
}.each_pair do |type, config|
(search_keys[type] || []).each do |word|
result = ecs_search(word, config, date_limit)
next if result.empty?
result.each do |prod|
products[prod[:asin]] = prod
end
end
end
検索を実行する部分については、もう少しマシなやり方がありそうに思う。たとえば対象が書籍についてはpower検索を使ったほうが効率が良くなるように思う。
iCalender 1.0.2についてだが、これは0.98のころとあまり状況が変わっていない。図書館で借りた本の返却日をGoogle Calendarに反映する その3で解説されているようなやり方で、VTIMEZONEを追加する必要がある。
ical = Icalendar::Calendar.new
ical.custom_property('X-WR-CALNAME', 'タイトル')
ical.custom_property('X-WR-CALDESC', 'タイトル')
ical.custom_property('X-WR-TIMEZONE', 'Asia/Tokyo')
std_cmp = Icalendar::Component.new('STANDARD')
std_cmp.custom_property('DTSTART', '19700101T000000')
std_cmp.custom_property('TZOFFSETFROM', '+0900')
std_cmp.custom_property('TZOFFSETTO', '+0900')
tvz_cmp = Icalendar::Component.new('VTIMEZONE')
tvz_cmp.custom_property('TZID', 'Asia/Tokyo')
tvz_cmp.add(std_cmp)
ical.add(tvz_cmp)
また、新たな問題もある。1.0.2ではiCalenderテキストの折り返しをきちんと(?)行うようになっているようで、マルチバイト文字はシーケンスの途中でも切り分けられてしまう。Googleカレンダーは、英数字ならばうまく連結してくれるようなのだが、マルチバイト文字は化けたままになるようだ。さらにこの折り返し処理は上書き可能な書き方になっていないため、最終的な出力を行った後で折り返し処理をやり直す形くらいでしか手を入れられない。
ical_text = ''
# 折り返された行を再連結し、折り返えし処理をやり直す
# (テキスト中の改行文字「"\n"」はiCalendarテキスト上では
# 「'\n'」に変換されているためこの処理の影響を受けない)
ical.to_ical.gsub(/\r\n /, '').strip.each_line do |l|
ical_text << utf8_fold(l, 70).join("\r\n ")
end
File.open(output_file + '.n', 'w') do |o|
o.print ical_text
end
新たな試みとしてPublish web content events in iCalに従って、商品イメージを表示できるようにしてみることにした。これは最初、VEVENTにイメージを添付するようなものかと思っていたのだが、実際には天気予報を表示する仕組みのことで、X-GOOGLE-CALENDAR-CONTENT-*が付いたVEVENTについては、他のカレンダーにコピーするなどの操作が(直接的には)出来なくなってしまうようだ。これはうれしくないので、イメージ表示用のエントリを別に作るという形で様子を見ている。
products.each do |asin, product|
date = product[:release]
desc = "...."
ical.event do
uid "X-ASIN-#{product[:asin]}"
dtstart date
dtend date.next
summary product[:short_title]
description desc
klass 'PUBLIC'
end
if image = product[:medium_image]
ical.event do
uid "X-ASIN-#{product[:asin]}-EVENT"
dtstart date
dtend date.next
summary product[:short_title]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-TITLE', product[:title]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-ICON', 'カレンダー上で表示する16x16のイメージのURL'
custom_property 'X-GOOGLE-CALENDAR-CONTENT-URL', image[:url]
custom_property 'X-GOOGLE-CALENDAR-CONTENT-TYPE', 'image/jpeg'
custom_property 'X-GOOGLE-CALENDAR-CONTENT-WIDTH', image[:width].to_s
custom_property 'X-GOOGLE-CALENDAR-CONTENT-HEIGHT', image[:height].to_s
klass 'PUBLIC'
end
end
end
便利かどうかはまだなんとも言えない(あまり便利ではないかも)。イメージを直接埋め込むのではなく、適切なリンクを仕込んだHTMLを生成して、それを埋め込むようにするとよいのかもしれないと思ったが、そこまではやってみていない(すでにありそう……)。
CGI環境、Webアプリケーション環境としてのRack
Thin(0.6.3)のことを調べていたらRack(0.2.0)が出てきて、Thin自体よりもむしろRackに興味が移っていった。Ruby 1.9系ではCGI環境の刷新が議論されている(はずだと思う)が、このRackを検討するのもおもしろいのではないだろうか。
Rackがどういうものかを考える前に、Mongrel-Railsの構成を少しだけかみくだいて見てみると[ブラウザ]-(HTTP)-[Mongrel]=[Rails]-(Rails規約)-[アプリケーション]という接続になっているといえる。これがThis-Rack-Rails構成でどうなるかというとブラウザ-(HTTP)-[Thin HTTPサーバ(Thinハンドラ]=[Rack]-(Rack仕様)-[Railsアダプタ]=[Rails]-(Rails規約)-[アプリケーション]のようになる。ここでRackの前後について、HTTP寄りについてはハンドラという繋ぎ役を作ることで拡張でき、アプリケーション寄りについてはアダプタという繋ぎ役を作ることで拡張できる。あるいは、Rack仕様はとてもシンプルなのでアプリケーションが直接Rackに接続することも考えられる。小さなアプリケーションならそれでも十分だろうと思う。
そのようなわけで、ThinとRackの組み合わせによれば、たとえばMongrel-Rack-Railsというパスを作れるはず。ちょっと作ってみるかという気分になったりしたのだが、あまり変なものは出てこなさそうな気配がしたので止めた。なお、Rack自体の使い方についてはCGI から Mongrel まで、Rack で Web アプリを Web サーバから抽象化するという記事で詳しく説明されている。
Thin自体はMongrel以降のRails向けHTTPサーバ実装として、とくにそのスピードを中心に取り上げられることが多いのかなという印象なのだけど、Rackを採用したこと、Rack仕様に従ったRails adapterを提供しているということ、initスクリプトを使った複数サービスの運用あたりまで考えられていること(ちょっとしたスクリプトを提供してくれるだけではあるのだけど)、なんてあたりも重要なポイントなのではないかと思う。

