くるくる回して入力するUIを作ってみた

とある範囲の値を入力してもらいたいけど, テキストボックスじゃ自由度高すぎるし, 範囲が微妙に広いからコンボボックスじゃ選ぶのめんどいなー*1.

そんなコトを考えているときに「こんなのどうよ?」と思って作ってみた.

とりあえずこんな風になった

このページを開いて, 青いボックスの上でマウスのボタンを押しっぱなしにすると, ちっこい矢印がでてくる. その状態で, その矢印を中心に:

  • 時計回りにドラッグするとカウントアップ
  • 半時計回りにドラッグするとカウントダウン

します.

欠点

  • 入力の仕方がわかりにくい*2
  • 入力範囲が広すぎると何回もくるくるしなくちゃいけないのでめんどくさい

とりあえず意外におもしろい動きなんじゃないかと思います. iPodのUI? 知らないお( ^ω^)*3

あ, もし使いたい方はこちらからJavaScriptのソースを取得してもらえればいいかと思います. ちなみにjQueryに依存しているのでご了承を.

あと例に使ったソースはこちらです.

*1:下三角クリックして, スクロールして, 目的の項目をクリックするっていう手順がなんか煩雑だと思うのです

*2:センターとなるマーカーを工夫すればいいと思うけど

*3:作っている途中で「iPodのような操作感だなー」と思いましたけど

とりあえずRackのミドルウェアとして実装すればいいんじゃない?

最近Rackの上にごくごく簡単なフレームワークSinatraも使わず組んでいるんですが, ミドルウェアが便利で, 仕組みもごくごく単純でステキだなぁとか思っているからとりあえずメモしておこうと思って久しぶりにブログを書こうとふにゃらららら.

仕組みから言えばフィルターとして動きます. たとえば:

use Rack::ShowExceptions
use Rack::Lint
run ExampleApp.new

ってミドルウェアを積むと, Rack::ShowExceptions#call(env) -> Rack::Lint#call(env) -> ExampleApp#call(env)と順番に呼ばれます. ただチェインしているだけですね. 素直な実装でボクうれしい.

つーコトでオプション的な機能は引数のoptionsとか設けて対応するんじゃなくて, Rackのミドルウェアに任せれば済むんじゃないか. 素直な実装というコトで, 簡単に作れました. 試しに行頭と行末のホワイトスペースおよび改行を削除するミドルウェアを書いてみた.

require 'rubygems'
require 'rack'

class Strip
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call env
    body, content_length = strip body
    headers['Content-Length'] = content_length.to_s
    [status, headers, body]
  end

  def strip(body)
    content_length = 0
    body.each do |s|
      s.gsub! /^\s+|\s+$|[\r\n]/m, ''
      content_length += Rack::Utils.bytesize s
    end
    [body, content_length]
  end
end

あとはuse StripってしてあげればOK. 簡単だね.

ただミドルウェアを積む順番を意識するのはめんどうなので, @app.call(env)を必ず呼び出して, その結果を加工する程度のモノにした方がよさそうです. 試しにキャッシュ機能とか実装してみたんですけど, キャッシュがあった時点で後方のアプリケーションを呼び出さないので, 順番によって結果が変わる可能性があるからです.

とりあえずミドルウェアで実装しとけばいいんじゃない? って話でした. Sinatraにも使えるしね.

Rubyで実践 OAuth in Twitter

というコトで前回のエントリに引き続き, OAuthです. 今度はコードを書き, OAuthを用いてTwitterのユーザータイムラインを取得してみようと思います.

コードはGitHubの方に置いてみました. 初めてのGitHubわくわくです.

OAuth examples

ちなみにターミナルから起動します. GitHubの方にはREADMEすら置いていないのであしからず.

$ ruby examples/twitter.rb

以下手順を追っていきますが, コードを部分ずつにわけて載せています. 全部が見たい人はGitHubの方で見てみてください.

認証トークンを取得する

consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
  :signature_method => OAuth::SignatureMethod::HMAC::SHA1
}

request_token = consumer.get_token REQUEST_TOKEN_URL

puts "Request token: #{request_token.token}"
puts "Request token secret: #{request_token.secret}"

まずコンシューマのキーと秘密の文字列でコンシューマオブジェクトを生成. Twitterは今のところ, signature_methodとしてHMAC-SHA1しか対応していないので, それも指定.

そしてOAuth::Consumer#get_tokenで認証トークンを取得.

Request token: REQUEST-TOKEN
Request token secret: REQUEST-TOKEN-SECRET

ユーザーを認証する

%x{open #{AUTHORIZE_URL % request_token.token}}
puts 'Press enter key after allowed.'
gets

最初に断っておきますと, openを使っているのでMac OS Xでしか動きません(たぶん). まぁAUTHORIZE_URL % request_token.tokenの結果を出力して, 手動でアクセスしてあげれば問題はないはず.

puts AUTHORIZE_URL % request_token.token
puts 'Press enter key after allowed.'
gets

このアクセスしたページでユーザーの認証と, リソースへのアクセス許可をもらう. Callback URLで指定したURLに飛ぶがそれは無視して, Enterキーを押す.

アクセストークンを取得する

consumer.token = request_token
access_token = consumer.get_token ACCESS_TOKEN_URL

puts "Access token: #{access_token.token}"
puts "Access token secret: #{access_token.secret}"

unless access_token.empty?
  puts 'Optional values:'
  access_token.each_pair {|key, value| puts "  #{key} => #{value}"}
end

さっき取得した認証トークン(request_token)をコンシューマにセット. 再度get_tokenを行い, 今度はアクセストークンを取得. ちなみにTwitterはこのとき付随的な情報として認証されたユーザーのuser_idとscreen_nameも返す. OAuthの仕様書を読んでいないが, こういうコトもできるという認識でOK?

Access token: ACCESS-TOKEN
Access token secret: ACCESS-TOKEN-SECRET
Optional values:
  user_id => 14976270
  screen_name => takkkun

サービスプロバイダ上のリソースにアクセスする

consumer.token = access_token
response = consumer.get 'http://twitter.com/statuses/user_timeline.json'

puts response.code
puts response.body

取得したアクセストークンをコンシューマにセットし, 任意のリソースにリクエストを送る. 今回はユーザータイムラインを取得.

200
[{"text":"\u3055\u3063\u304d\u304b\u3089\u4e0b\u307e\u3076\u305f\u306b\u76ee\u85ac\u3057\u3066\u308b", ~

ちなみにアクセストークンとシークレットを保存しておけば, 次から認証トークンの取得からアクセストークンの取得のステップを飛ばして, いきなりリソースへのアクセスを行うコトができる.

consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, {
  :signature_method => OAuth::SignatureMethod::HMAC::SHA1,
  :token            => OAuth::Token.new('ACCESS-TOKEN', 'ACCESS-TOKEN-SECRET')
}
response = consumer.get 'http://twitter.com/statuses/user_timeline.json'

puts response.code
puts response.body

出力は上記と同じ.

取れました

というコトでOAuthによるリソースの取得ができました. 手順を追うといっても上っ面だけなので, 中で何をして, どんなリクエストを送っているかはoauth.rbでも見ていただければわかると思います.

さすがにいろいろ読んだし, コードも書いたので, 最初の流し読みのときよりもかなり理解が深まったかなー. 

OAuthを知る

どうやらOAuthというモノがあるらしい. 仕組みを考えだしたのはTwitterの中の人だとか.

そのOAuthについて理解を深めるために, 粛々とつづってみる. 間違っていたら突っ込んでいただけると嬉しいです.

OAuthの目的

OAuthは認証と認可を目的とする. OpenIDは認証だけなので, そこがOAuthとの違い.

この認可とは, 特定のコンシューマが, 認証されたユーザーの同意によって, サービスプロバイダ上にあるリソースへアクセスできるようになる, というモノ.

OAuthの登場人物

サービスプロバイダさん

ユーザーの認証と, リソースへ対するコンシューマからのアクセスを認可するサービスのコト. OpenIDでいうOpenID Providerの立場.

くだけて言うとOAuthサーバーみたいな感じ. 例えばTwitterとか.

コンシューマさん

リソースへのアクセスを, ユーザーの同意の上で行うサービスのコト. サービスと書いたが, 別にデスクトップアプリでもいい. OpenIDでいうRelying Partyの立場.

くだけて言うとOAuthクライアントみたいな感じ. 例えばTwitter用のデスクトップで動作するクライアントとか.

ユーザーさん

コンシューマから「ちょっとサービスプロバイダ上のリソースにアクセスしたいので, 許可してもらえるかな?」と言われる人.

OAuthの仕組み

リソースへのアクセスは認可もそうだが, まず認証されていなければならないので, たいていIDとパスワードが必要となる. そこをOAuthはトークンと言うランダムな文字列で代用するようにして, リソースへのアクセスを認めている, って感じ. このトークンは2種類ある.

認証トーク

まず「このURLにアクセスして, リソースにアクセスするのを許可してもらえるかな?」という時に必要となる認証トークン(request token). これは簡単に取得できる.

ちなみにこのタイミングでサービスプロバイダ側がログイン画面を用意するので, そこでユーザーにIDなりパスワードなりを入力してもらって, 認証を行う. 実際試してみるまでどこで認証がされるかがわかりづらかった……

そしてリソースへのアクセスの許可/拒否もしてもらう. 認証トークンの出番はこれだけ.

アクセストーク

次にリソースへのアクセスに必要となるアクセストークン(access token). この認証トークンとアクセストークンはユーザーの認証が済んで, さらに許可してもらった後なら取得できる(イメージ的には認証トークンと交換するイメージ).

そしてこのアクセストークンは何度でも使う. リソースへのアクセスに認可を下すには, アクセストークンが妥当かどうかで判断している. よって一度認可をもらえばリソースへのアクセスはツーツーというわけではなく, アクセストークンを毎回含め, 毎回認可してもらう必要がある.

OAuthを使うための準備

まずサービスプロバイダに「こういうコンシューマを作るので, お願いします」という登録が必要. それでコンシューマのキーと秘密の文字列を貰う. 秘密の文字列は署名(Signature)を生成するのに必要で, キー自体は認証トークンをもらうときも, アクセストークンを貰うときも, それ以後のアクセスを行うときも……, すなわちすべてのリクエストに含めなきゃいけない. でないと, どのコンシューマからのアクセスかわからないから.

というわけで, Twitterでコンシューマを登録してみる. まずhttp://twitter.com/oauth_clientsにアクセス.

"Register a new application »"のリンクをクリックして, コンシューマの情報を入力する.

特に重要なのがCallback URL. ユーザーの認証が済んだ後, ここに記入したURLにリダイレクトされるようになっている. なので大体はアクセストークンの取得を行うURLを書くコトになると思う.

そして登録したコンシューマの情報が記載されているページ.

Consumer keyがコンシューマのキー. Consumer secretが秘密の文字列となる. あと認証トークンとアクセストークンを取得するさいに用いるURL(Request token URL, Access token URL)と, ユーザーの認証およびリソースへのアクセス許可をしてもらうためのURL(Authorize URL)も書かれている.

これらの情報でサービスプロバイダとやりとりできる. ひとまずコンシューマの登録は完了したので, 準備はコレでおしまい.

次からコードを書いて実践してみる.

Rubyで実践 OAuth in Twitter

RailsでAmazonの商品を, 署名認証に対応した上で扱う

今朝こんなメールがきました.

【重要】Product Advertising API (旧 AmazonアソシエイトWebサービス)の署名認証に関するお知らせ

内容をサラッと言うと:

うちが調べてみたところ, おたくのAWSAccessKeyIDから送られてくるリクエストに電子署名がついていないんですよー.
んでー, 8月15日までに電子署名つけてくれないと, リクエスト受け付けなくなっちゃうんですよー.
だからそれまでにつけておいてね!

です.

いやー, やっぱり催促されちゃいますよね. というわけで署名認証に対応したのもメモメモ.

ruby-aawsのインストール

前回のバージョンで使ったamazon-ecsは署名認証に対応していない(だからメールがきたんだけど)ので, 別のRubyGemsを用意. 前回のバージョンのブコメid:takahashimさんが:

対応済みをうたっている(未確認)ruby-aawsの方がいいような

とおっしゃっていたので, 早速ruby-aawsをインストール.

$ sudo gem install ruby-aaws

ちなみにruby-awsというのもあって, それを間違えて入れていたのは内緒. ruby-awsではなく, ruby-aawsなので注意(自分に言い聞かせるように).

デフォルトの設定を書く

いつものようにこのRubyGemsを使いますよー, っていうのを書く.

# config/environment.rb
config.gem 'ruby-aaws', :lib => 'amazon'

あとデフォルトの設定(AWSAccessKeyIDなど)を書く. 専用のファイルに書くので:

# config/amazonrc
[global]
  key_id        = APIを使うために必要なアクセスキー
  secret_key_id = 秘密キー
  locale        = jp
  cache         = true
  cache_dir     = tmp/amazon

って書いて:

# config/initializers/amazon_aws_env.rb
ENV['AMAZONRCDIR']  = 'config'
ENV['AMAZONRCFILE'] = 'amazonrc'

そのファイルを読みにいくよう, 環境変数を設定しておく.

何気に素敵なのが, このRubyGems自体にキャッシュの機能がついているコト. config/amazonrcのcacheとcache_dirがそれ. キャッシュの保存場所はtmp/amazonにしたので:

$ mkdir tmp/amazon

そのためのディレクトリを作っておく.

モデルを書く

class AmazonItem < ActiveRecord::Base
  validates_presence_of :asin

  def method_missing(name, *args)
    lookup unless @looked
    @item.send name, *args
  end

  protected

  def validate
    lookup unless @looked
    errors.add :asin, "can't find the item from Amazon" unless @item
  end

  private

  def lookup
    page = Amazon::AWS::Search::Request.new.search(
      Amazon::AWS::ItemLookup.new('ASIN', 'ItemId' => asin),
      Amazon::AWS::ResponseGroup.new('Medium')
    )
    @item = page.item_lookup_response.items.item
    @looked = true
  end
end
amazon_item = AmazonItem.find_by_asin '4797336617'
puts amazon_item.item_attributes.title.to_s # => たのしいRuby 第2版 Rubyではじめる気軽なプログラミング

リクエストされたURLは:

http://ecs.amazonaws.jp/onca/xml
?AWSAccessKeyId=XXX
&AssociateTag=calibanorg-20
&IdType=ASIN
&ItemId=4797336617
&Operation=ItemLookup
&ResponseGroup=Medium
&Service=AWSECommerceService
&Timestamp=2009-06-17T09%3A46%3A55Z
&Version=2009-03-31
&Signature=g42E4Xoj%2F9c1peqybJxnN0iuxB8Cr4ZVPwggPcI0SHw%3D

なので, ちゃんと電子署名(Signatureパラメーター)が付与されていますよ, と*1.

ちなみにmemcachedを使わなくても, キャッシュを使うように設定すればruby-aawsだけでもそこそこ速い. あとItemLookup以外のオペレーションもキャッシュするので何かと便利かも.

ただ速度はオンメモリなmemcachedに及ばない(未確認)だろうし, ネットワークを介すこともできないのでそこはケースバイケースかな.

あと気になるのが:

Amazon::AWS::Search::Request.new.search(
  Amazon::AWS::ItemSearch.new('Books', 'Keywords' => 'ruby'),
  Amazon::AWS::ResponseGroup.new('Medium'),
  5
)

ってやると, 1ページ目から5ページ目までの検索結果が返ってくる点. 5ページ目だけじゃなくて?

*1:ちなみになんでAssociateTagがついてんの? つけていないのに……

"この世で最も役に立たない関数"をRubyのメソッドで

寝つけないところにこのエントリ. defを使う方法でやってみた.

def pointless
  puts "Turing off myself = #{method :pointless}"
  undef :pointless
end

pointless
pointless

Rubyにもundefがあるなんて初めて知ったぜ.

追記: Pythonでも書いてみた

vars = vars()

def pointless():
    print 'Turning off myself = ' + pointless.func_name
    del vars[pointless.func_name]

pointless()
pointless()

んー, うまく__main__モジュールの辞書を取得できないだろうか……

また追記(11:57): Pythonにはglobalなるものがあるらしい.

ブクマコメントなどを見ると, globalな空間を参照できるとのコト. この感じを見てPHPを思い出して, 少し感慨深くなった(個人的な話).

def pointless():
    global pointless
    print 'Turning off myself = ', pointless # func_name使わなくてもコレでいいっぽい
    del pointless

pointless()
pointless()

またひとつ賢くなったよ! 多用すべきものではないと思うけど.

RailsでAmazonの商品を扱う

Amazonの商品はAPIを介して簡単に取得できますが, 取得した情報はキャッシュ目的以外でローカルに格納してはいけない, などの制約がある. なのでどんな風に書けばいいかメモ. あと毎回APIを介していては遅くなるのでmemcachedを使う(キャッシュ目的なのでOKなはず).

まずAmazonAPIに簡潔にアクセスできるRubyGemsを入れる.

$ sudo gem install amazon-ecs

次にRailsのconfigの中身を書き換えておく.

# config/environment.rb
config.gem 'amazon-ecs', :lib => 'amazon/ecs'
# config/initializers/amazon_ecs.rb
Amazon::Ecs.options = {
  :aWS_access_key_id => 'APIを使うために必要なアクセスキー',
  :country           => :jp,
  :response_group    => 'Medium'
}

そしてマイグレーションファイル.

class CreateAmazonItems < ActiveRecord::Migration
  def self.up
    create_table :amazon_items do |t|
      t.string :asin, :null => false
      t.timestamps
    end
  end

  def self.down
    drop_table :amazon_items
  end
end

ASINのみは恒久的に保持しておいていいのでこうする(他のを含めると規約違反なはず).

後はモデル.

class AmazonItem < ActiveRecord::Base
  validates_presence_of :asin

  def get(path)
    lookup unless @looked
    @item && @item.get(path)
  end

  protected

  def validate
    lookup unless @looked
    errors.add :asin, "can't find the item from Amazon" unless @item
  end

  private

  def lookup
    @item = Amazon::Ecs.item_lookup(asin).first_item
    @looked = true
  end
end

必要なときだけlookupメソッドでAmazonに要求するようにし, 2回目以降は当然リクエストしないようにしておく.

コレで:

amazon_item = AmazonItem.find_by_asin '4797336617'
puts amazon_item.get 'itemattributes/title' # => たのしいRuby 第2版 Rubyではじめる気軽なプログラミング

こんな感じに取得できる.

だけどASINが同一でも, インスタンスが違えば@lookedインスタンス変数は偽となる(nil)なので, そういう場合はやっぱり速度が落ちる. というコトでキャッシュを導入.

$ sudo port install memcached
$ sudo gem install memcache-client
class AmazonItem < ActiveRecord::Base
  CACHE_SERVERS = 'localhost:11211'

  validates_presence_of :asin

  def get(path)
    lookup unless @looked
    @item && @item.get(path)
  end

  protected

  def validate
    lookup unless @looked
    errors.add :asin, "can't find the item from Amazon" unless @item
  end

  private

  def lookup
    unless @@cache
      require 'memcache' # Amazon::Ecsはrequire 'amazon/ecs'しなくても使える(それはそれで怪しい)が……
      @@cache = MemCache.new CACHE_SERVERS
    end
    if @item = @@cache[asin]
      @item = Amazon::Element.new Hpricot @item
    elsif @item = Amazon::Ecs.item_lookup(asin).first_item
      @@cache.add asin, @item.to_s, 24 * 60 * 60
    end
    @looked = true
  end
end

あとconfig/environment.rbにも追記する.

config.gem 'memcache-client', :lib => 'memcache'

コレだけで劇的に早くなった. 発想は単純だけど, 単純だからこそなのか, キャッシュはかなり効果的だなぁ. あとAPIを介して取得した情報はキャッシュしてもいいが, 1日だけという期限がついている. それもmemcachedを使えば, うまい具合にできてしまう. コレはいいや.

気になるところ

  • MacPortsで入れたmemcachedが1.2.8と古い(今は1.4とか?). やっぱりソースから入れた方がいいかな
  • 上記ではAmazonItemクラスひとつにつきmemcachedのクライアントを作っているが, そのスコープは適切なのか. ローカル変数に入れて, その場その場でok? それとももっと大きなスコープでもok?
  • serialize/desirializeが強引. addや[]はMarshal.dump/loadを行うが, uninitialized structと例外が発生してしまうし……

actionscript-modeのロード時にエラー

久しぶりにActionScriptでなんか書こうと思ったら, Emacsで*.asファイルを開いても, シンタックスハイライトされない! *Messages*を読むと:

Loading /Users/takkkun/elisp/actionscript-mode.el (source)...
File mode specification error: (void-function c-identifier-re)

エラーだ. actionscript-modeを使いたいのに, actionscript-mode.elでどうにかなっているっぽい. c-identifier-reという関数がマズいのかな?

以前まで使えていたのになぁ. ちなみにバージョンは:

というコトで調べてみるっす.

追記(同日 12:49): 解決!

原因はごくごく単純. actionscript-modeが必要とするcc-modeを, 別の*.elが既に読込んでしまっていたのが原因でした. ちなみに問題であろうコードは下記のやつ.

(when (load "js2" t)
  (setq c-basic-offset 2
        js2-basic-offset 2
        js2-mirror-mode nil
        js2-electric-keys nil
        js2-strict-missing-semi-warning nil
        js2-strict-inconsistent-return-warning nil))

js2-modeの挙動を変えるために書いたコードが…… たしかに前回actionscript-modeで書いた後にこのコードを書いたと思う. でもこの時点でcc-modeを読込んじゃうってどうなんだろう?

before_filterでfalse返しているのにアクションが実行されるコトがある

以前ちょろーっと読んだ本には「before_filterに指定したブロックやメソッドが評価された結果, falseを返した場合, フィルターのチェインはそこで終わるよ!」って書いてあった(と思う).

それを信じてやってみたら, なんとチェインが終わらない(そのままアクションが実行される)!

class HogeController
  before_filter :fuga

  private

  def fuga
    false
  end
end

終わらないのはボクの記憶違いなのか, それともRailsのバージョンによるものなのか…… そしてふと思う. チェインが終わった時点でレスポンス(render, redirect_to)が決まっていないときはどうなるんだろう, って. あー, なんとなくわかったような……

そしてrenderしてみる.

class HogeController
  before_filter :fuga

  private

  def fuga
    render
    false
  end
end

お, 止まった! いまさらながらにソースを見てみる.

# actionpack-2.3.2/lib/action_controller/filters.rb
# 略
def call_filters(chain, index, nesting)
  index = run_before_filters(chain, index, nesting)
  aborted = @before_filter_chain_aborted
  perform_action_without_filters unless performed? || aborted
  return index if nesting != 0 || aborted
  run_after_filters(chain, index)
end
# 略

(あんまり詳しく見ていないのでおそらくだけど)perform_action_without_filtersがアクションのみの実行だと思う. そしてそれを実行する条件にperformed?(レスポンスが決定しているかどうか)が含まれているからかなぁ. てかperformed?がtrueの時点でアクションを実行しないのなら, falseを返す必要ないんじゃない?

というコトで下記のコードでもアクションは実行されないようです.

class HogeController
  before_filter :fuga

  private

  def fuga
    render
  end
end

でもbefore_filterが多重に定義されていたらどうなるかは細かく試していないです(ちょろっと試した感じでは止まっていたと思いますけども……).