今作っているサービスでRuby 2.0.0を使っているので、開発サーバのRVMをアップデート(1.16.6 → 1.20.5)して、Ruby 2.0.0を入れてみました。

# 安定板を入れるならrvm get stable
rvm get latest

# 呼ばなくても大丈夫そうですが
rvm reload

# Ruby 2.0.0のインストール
rvm install 2.0.0

# 2.0.0に切り替えて、バージョン確認
rvm 2.0.0
ruby -v
 => ruby 2.0.0p0 (2013-02-24 revision 39474) [x86_64-linux]


いい感じですね!

Posted by あかさた
今更感が途方もなく漂いますが、気付いていなかったので。。。

Ruby 1.8では、レシーバのオブジェクトを取り出すにはブロック引数を使っていましたが、Ruby 1.9ではselfを使って取り出すようです。

class MyClass
  def hello
    puts "Hello World!"
  end
end

MyClass.new.instance_eval do
  hello
end


あまり使わないメソッドですが、最近使う機会があったので。
mrubyでもRuby 1.9と同様の動作でした。

■ 参考


Posted by あかさた
仕事でImageMagickで画像処理をしていたのですが、中間ファイルをいろいろ作る必要があったので、ファイルの掃除も考慮してくれるTempfileを組み合わせて使ってみました。

  • テスト環境
    • Ruby 1.9.3
    • RMagick 2.13.1
    • ImageMagick 6.7.9-10
  • Railsを使っているとアップロードされたファイルはTempfileとして扱われる
    • サイズによってはStringIOとして扱われていた時代もあった気がしますが・・・昔の話のはず
  • 画像処理で中間ファイルを作る場合、ごみを残さないTempfileは便利

require 'RMagick'
require 'tempfile'

# アップロードされたファイル
uploaded_file = params[:file]

# TempfileからImageを作成する
# from_blobはArrayを返すので、shiftして先頭の要素を取得
image = Magick::Image.from_blob(File.read(uploaded_file.path)).shift

# 画像フォーマットの確認
puts image.format

# なんかいろんな処理

# Tempfileの作成
tempfile = Tempfile.open(temp_filename)

# Tempfileへの書き込み
# フォーマットの指定が便利
image.write("png:" + tempfile.path)

# なんかいろんな処理

# Tempfileの削除
tempfile.close!


■ 参考


Posted by あかさた
メソッドのオーバーライドを禁止してみたい(というかjavaでいうところのfinalをやりたい)と思ったので、いろいろ調べてみたところ、クラスの定義に関するフックmethod_addedとinheritedを使うとできそうなので、コードを書いてみました。

  • method_addedでメソッドの定義が行われると、そのメソッドを削除して例外を発生させる
    • 継承してmethod_addedを上書きすると上記チェックを回避できてしまう
  • inheritedで例外を発生させて継承を禁止する
    • inheritedのチェックだけだと、同一クラスの再定義でメソッドを上書きできてしまう

method_addedでremove_methodをすると、上書き前のメソッドも削除されてしまうのが、ちょっと不便ですね。追加前をフックできるといいのですが。。。

module FinalClass
  class << self
    def included(base)
      base.extend ClassMethods
    end
  end
  
  module ClassMethods
    def method_added(name)
      remove_method name
      raise TypeError, "You cannot define a new method!"
    end
    
    def inherited(subklass)
      raise TypeError, "You cannot define a subclass!"
    end
  end
end

class A
  include FinalClass
end


ま、実務で使うかどうかといわれるとわかりませんが・・・。

■ 参考

本記事は上記2記事を足して割った感じです。


freezeの利用も検討しましたが、いろいろと抜け穴ができてしまったので断念しました。

  • クラスをfreezeすると、メソッドの再定義もできなくなる
  • ただし、いくつか抜け道がある
    • 継承するとメソッドのオーバーライドが可能(inheritedでサブクラスをfreezeするアプローチもあるが・・・)
    • クラスをdupするとメソッドのオーバーライドができてしまう(cloneはfreezeを引き継ぐので問題ない)

Posted by あかさた
このブログにAmazonのリンクを貼りたくなったので、ASINを入力するとAmazonから書籍のサムネイルや著者情報などを抜き出す機能を実装するために、Amazon Product Advertising APIを使ってみることにしました。

■ インストール

gemなので、コマンド一発です。

gem install amazon-ecs


■ 基本設定とAPI呼び出し

require 'amazon/ecs'

Amazon::Ecs.options = {
  :associate_tag => '[your associate tag]', 
  :AWS_access_key_id => '[your developer token]', 
  :AWS_secret_key => '[your secret access key]'
}

# API呼び出し
res = Amazon::Ecs.item_lookup("4088767624", :response_group => 'Small, ItemAttributes, Images', :country => 'jp')

# 返ってきたXMLを表示(res.doc.to_sでも多分OK)
puts res.marshal_dump


item_lookupメソッドに限りませんが、response_groupの設定は注意が必要です。Imagesを入れないとサムネイルのURLが、ItemAttributesを入れないと出版日などの情報が取れませんでした。

■ 情報の読込

適当に取り出しています。商品ページのURLがやたらと長くなってDBに入れたくない感じになったので、適当にURLを作ってしまいました。一応、タグが機能しているかAmazonアソシエイトのリンクの動作確認ツールで確認してあります。

amazon_tag = '[your associate tag]'

res.items.each do |item|
  element = item.get_element('ItemAttributes')
  
  data = {
    :asin => item.get('ASIN'), 
    :title => element.get("Title"), 
    :page_url => "http://www.amazon.co.jp/dp/#{item.get('ASIN')}?tag=#{amazon_tag}", 
    :isbn => element.get("ISBN"), 
    :author => element.get_array("Author").join(", "), 
    :product_group => element.get("ProductGroup"), 
    :manufacturer => element.get("Manufacturer"), 
    :publication_date => element.get("PublicationDate"), 
    
    # URL, Width, Heightの要素を持っている
    :small_image => item.get_hash("SmallImage"), 
    :medium_image => item.get_hash("MediumImage"), 
    :large_image => item.get_hash("LargeImage")
  }
end

# dataを表示する(省略)


■ 結果

以下のような感じで表示してみました。皇国の守護者ですな。古い作品ですが、なんで打ち切りになってしまったのか・・・。この作家が今描いているシュトヘルも面白いんだけど、このころの勢いにはなかなか追いつかないしなぁ・・・。

[書籍データの取得に失敗しました]

■ 感想

プログラム的に難しい個所はありませんが、Amazon Product Advertising APIをある程度把握しておく必要があるでしょう。

APIの呼び出し回数の制限(1時間2000回? 収益による増減もあるので少し難しい)を考慮してキャッシュしたり、ProductGroupによってとれる情報が異なる(例:本では著者が取れるが、ゲームでは取れないなど)ので、そういった差異を吸収するような表示を検討したりと、ちゃんとしたものを作る場合はそれなりに手間がかかるかと思われます。

■ 参考情報


Posted by あかさた
複数のアプリから参照するライブラリを書きたくなったので、勉強がてらgemを作ってみることにしました。gemテンプレートを生成して、処理を記述して、rspecでテストを書くところまでを書いています。

■ テンプレートの生成とgemspecの記述

以下でテンプレートの生成ができます。

bundle gem hello-akasata


hello-akasata.gemspecの内容です。TODOと書いてあるところを書き換えましょう。

# -*- encoding: utf-8 -*-
require File.expand_path('../lib/hello-akasata/version', __FILE__)

Gem::Specification.new do |gem|
  gem.authors       = ["akasata"]
  gem.email         = ["*@*.*"]
  gem.description   = %q{Test Gem.}
  gem.summary       = %q{Hello World!}
  gem.homepage      = ""

  gem.files         = `git ls-files`.split($\)
  gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
  gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
  gem.name          = "hello-akasata"
  gem.require_paths = ["lib"]
  gem.version       = Hello::Akasata::VERSION
  
  # 依存するgemがある場合
  # gem.add_dependency('log4r', '>= 1.0.5')
  gem.add_development_dependency('rspec')
end


lib/hello-akasata/version.rbの内容は以下の通りです。内容を変更したらここを変更しましょう。

module Hello
  module Akasata
    VERSION = "0.0.1"
  end
end


■ 処理の記述

lib/hello-akasata.rbの内容を書き換えてください。とりあえず、hello worldです。

require "hello-akasata/version"

module Hello
  module Akasata
    def self.hello
      "Hello akasata World!"
    end
  end
end


■ ビルド

rakeでビルドを行うことができます。

rake build # ビルドします
rake install # ビルドしてシステムにインストールします
rake release # これでRubyGems.orgに公開されます。今回はやってませんw


■ rspec

spec/spec_helper.rb

require 'rubygems'
require 'bundler/setup'
require 'hello-akasata'

RSpec.configure do |config|
  config.mock_framework = :rspec
end


spec/hello-akasata_spec.rb

require 'spec_helper'

describe Hello::Akasata, "::hello" do
  it "should return 'Hello akasata World!'" do
    Hello::Akasata.hello.should eq("Hello akasata World!")
  end
end


以下、rspecの実行です。

rspec


■ 参考文献

ASCIIcasts - “Episode 245 - Bundlerでgemを作る”
testing - Setup RSpec to test a gem (not Rails) - Stack Overflow

Posted by あかさた
Ruby(Rails)からメールを送信する必要のある仕事があったので、本採用するかは決めていませんが、Amazon SESをaws-sdk-rubyから使う方法について調査してみました。

■ 準備

1. マネジメントコンソールでEmailアドレスとドメインを登録してください
(ドメインの登録の際は、DKIMの設定も出力すると便利です。Easy DKIMという機能なのですが、ドメインの認証が完了した後、マネジメントコンソールで有効化すると、送信メールにDKIM署名を追加してくれます。迷惑メール扱いになる確率がいくらか減るでしょう。)

※ Request Production Accessするまでは、送信元、送信先ともに登録が必要です。

2. aws-sdk-rubyをインストールする

gem install aws-sdk


■ 手順

単純なので一気に書きます。

# encoding: utf-8
require 'rubygems'
require 'aws-sdk'
require 'nkf'

ses = AWS::SimpleEmailService.new(
  :access_key_id => 'YOUR_ACCESS_KEY_ID',
  :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')

body_text = NKF.nkf '-jw', <<-EOS
こんにちは。
このメールはテストメールです。

おしまい
EOS

subject = NKF.nkf('-Mw', 'サブジェクトです')

from_email = "FROM_EMAIL_ADDRESS"
to_email = "TO_EMAIL_ADDRESS"

p "Verified?(FROM Email): " + ses.identities[from_email].verified?.to_s
p "Verified?(TO Email): " + ses.identities[to_email].verified?.to_s
p "Verified?(Domain): " + ses.identities['YOUR_DOMAIN'].verified?.to_s

ses.send_email(
  :subject => subject,
  :to => to_email,
  :from => from_email,
  :body_text => body_text,
  :body_text_charset => 'ISO-2022-JP'
)


■ 参考情報

RubyからAmazon SESでメールを送る - プログラマになりたい

詳しいです。ただし、Easy DKIMに関する記載が無い(この記事時点ではEasy DKIMがなかったのかも)ので、DKIM関係は独自署名の方法になっています。これはこれで押さえておいた方がいい情報ですが、Easy DKIMを使う方が楽ではあると思います。

AWS SDK for RubyのAPIリファレンス
AmazonSES-Easy DKIM設定サポート資料

Posted by あかさた
仕事でRubyでAmazon S3にアクセスする必要があったので、公式のgemであるaws-sdkを使ってみました。このgemはS3だけではなく、一通りのサービスにアクセスできるようなので、使いこなせばかなり有用かと思われます。

この記事では、S3へのファイルのアップロードを中心に以下の内容を扱っています。

・インストール
・基本設定
・ファイルのアップロード
・ファイルの暗号化
・アクセスコントロールの設定

■ インストール

gem install aws-sdk


■ 基本設定

アクセスキーを指定して、S3のインスタンスを生成します。

require 'aws-sdk'

AWS.config(
  :access_key_id => 'YOUR_ACCESS_KEY_ID',
  :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')

s3 = AWS::S3.new

# 以下も可
# s3 = AWS::S3.new(
#   :access_key_id => 'YOUR_ACCESS_KEY_ID',
#   :secret_access_key => 'YOUR_SECRET_ACCESS_KEY')


■ ファイルのアップロード

ファイルをS3に格納します。

bucket = s3.buckets['yourbucketname']

filename = "fullpath/filename.ext"
basename = File.basename(filename)
o = bucket.objects[basename]
o.write(:file => filename)


■ ファイルの暗号化(クライアントサイド)

サーバーサイド(S3)でもできますが、とりあえずクライアントサイトでも簡単なので紹介します。

# キーの生成
my_key = OpenSSL::Cipher.new("AES-256-ECB").random_key

# オブジェクトの取得
o = bucket.objects[name]

# 暗号化して書込
o.write("MY TEXT", :encryption_key => my_key)

# 復号化して読込
puts o.read(:encryption_key => my_key)


公開鍵暗号を使いたいなら以下の通り。公開鍵で暗号化して、秘密鍵で復号化します。

# キーの生成
my_key = OpenSSL::PKey::RSA.new(1024)

# オブジェクトの取得
o = bucket.objects[name]

# 暗号化して書込
o.write("MY TEXT", :encryption_key => my_key)

# 復号化して読込
puts o.read(:encryption_key => my_key)


■ アクセスコントロールの設定

S3Objectクラスのacl=メソッドを使うと、アクセスコントロールを設定してS3上のオブジェクトに反映することができます。

o = bucket.objects[name]

# Canned ACL
o.acl = :public_read

# writeメソッドに渡すことも可能
o.write(:file => filename, :acl => :public_read)


以下、渡せるアクセスコントロールポリシーです。

  • :private
  • :public_read
  • :public_read_write
  • :authenticated_read
  • :bucket_owner_read
  • :bucket_owner_full_control

acl=には、AccessControlListを代入することもできます。changeメソッドを使ったやり方もあります。

o.acl.change do |acl|
  acl.grant(:full_control).to(:canonical_user_id => "...")
end


S3ObjectのchangeメソッドはS3Objectが返すAccessControlListのインスタンスが持つ特異メソッドなので、注意してください。ブロックの処理が完了すると、S3上のオブジェクトに反映します。(具体的にはS3Objectのacl=メソッドを呼び出します。)

ACLに関しては以下を参照してください。

acl=メソッド(AWS::S3::S3Object)
http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html#acl%3D-instance_method

AWS::S3::AccessControlList
http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/AccessControlList.html

■ 参考情報

Getting Started with the AWS SDK for Ruby(AWS)
AWS SDK for RubyのAPIリファレンス
aws/aws-sdk-ruby(GitHub)
aws-sdkのファイルアップロードのサンプル
AWS SDK for RubyでS3の操作をする - プログラマになりたい(ファイルの取得関係が詳しい)

Posted by あかさた
以下の記事を読むと、DCIはRubyでは遅いという指摘があったので、移譲で
実装すればいいと思ったので、下記記事のサンプルコードをもとに、
send/method_missing, forwardable, delegateを使ったサンプルを書いて
みました。

※ 上記記事では移譲を使ったアプローチにも言及しているし、改善するには
頑張らないといけないという程度の話と認識しました。

■ ベンチマークの結果(ruby 1.9.3p194)

         without dci  3024223.4 (±1.7%) i/s -   15139474 in   5.007655s
            with dci   768491.0 (±6.6%) i/s -    3833440 in   5.011732s
      with dci(send)  1586461.1 (±2.1%) i/s -    7977320 in   5.030680s
with dci(forwardable)
                      1712079.6 (±1.8%) i/s -    8580546 in   5.013597s
  with dci(delegate)  1134862.9 (±1.3%) i/s -    5701904 in   5.025205s


見にくくて済みません。コードは下の方にあります。

■ 思ったこと

元記事のベンチマークは、オブジェクトの生成コストくらいしかないような
処理にextendを突っ込んでいるため、そりゃ比較にならないだろうという
問題があります。

そもそも、処理が無いならDCIを入れる意味なんてほとんどないことを考えれば、
ここのコストは無視してもいいものなのですが、無視してもいいコストをいかに
削るかという、無駄に挑戦することも一興かなと思い、記事にしました。

fooメソッドの処理内容によっては(※)、extendを使った元記事のアプローチの
方が優秀な場合もあると思います。柔軟性などを考えれば、移譲の方が使い勝手が
いいだろうとも思いますが。

※ 単純な移譲にならずに、Double Dispatchになるとオーバーヘッドで遅くなる
可能性もあると思います。

さて、ベンチマーク結果に移ると、forwardableが意外と速い。先に書いた通り、
オブジェクト生成コストしかないような処理に対して、半分程度の速度低下で
DCIが実現できるということです。これは、RubyにおけるDCIの実用性を示す
ものではないでしょうか。

■ 余談

これ、DCIじゃなくてRoleObjectじゃんって突っ込みが入りそうですが、
DCIで設計したらRoleObjectで実装するのが定番かなと思ってます。あとは、
Decoratorかな。やったことないけど。

しかし、皆DCIに突っ込んでいくとか、勇敢すぎると思う。Coplienの名前が
出たら全力で逃げるところだろう。マルチパラダイムデザインの恐怖を
忘れたんだろうか。。。(ネタですよ。念のため。)

■ ソースコード

require 'rubygems'
require 'benchmark/ips'
require 'forwardable'
require 'delegate'

class ExampleClass
  def foo; 42; end
end

module ExampleMixin
  def foo; 43; end
end

class ExampleClassSend
  def initialize(role)
    @role = role
  end
  
  def method_missing(action, *args)
    @role.__send__(action, *args)
  end
end

class ExampleClassForwardable 
  extend Forwardable
  
  def initialize(role)
    @role = role
  end
  
  def_delegator :@role, :foo, :foo
end

class ExampleClassDelegate < SimpleDelegator
end

class ExampleRole
  def foo
    43; 
  end
end

@role = ExampleRole.new

Benchmark.ips do |bm|
  bm.report("without dci") { ExampleClass.new.foo }
  
  bm.report("with dci") do
    obj = ExampleClass.new
    obj.extend(ExampleMixin)
    obj.foo
  end
  
  bm.report("with dci(send)") do
    ExampleClassSend.new(@role).foo
  end
  
  bm.report("with dci(forwardable)") do
    ExampleClassForwardable.new(@role).foo
  end
  
  bm.report("with dci(delegate)") do
    ExampleClassDelegate.new(@role).foo
  end
end


Posted by あかさた
Ruby 1.9 のソースコード読書会が 7/6(日)にミラクルリナックスさんの会議室(新橋)であります。次回は RHG で言うところの「第10章 パーサ」に相当する部分を勉強します。参加表明は以下のサイトでできます。

第6回 RHGの逆襲 - RHG片手にRuby 1.9を読む集い
http://qwik.jp/rhg-strikes-back/75.html

■ 7/3 17:50 追記

本勉強会は 7/27 に延期になりました。よろしくお願いします。m(_~_)m

Posted by あかさた