メタプログラミングRuby第2版 / 第6章コードを記述するコード[勉強メモ]


メタプログラミングRubyを読んでいます。完全自分用のメモ記事です。
第5章の「クラス定義」で特に参考になった部分を中心に書いています。


🚜 6章クラス定義

オブジェクトの汚染

外部から来た安全ではないをブジェクを「オブジェクトの汚染」という。
この汚染を判定するメソッドが#tainted?である。

user_input = "User input: #{gets()}"
puts "user_input.tainted? => #{ user_input.tainted? }"

only_code_object = 1
puts "only_code_object.tainted? => #{ only_code_object.tainted? }"

# ruby 6.2.4.tainted_code.rb
# <= 1< span>
# user_input.tainted? => true
# only_code_object.tainted? => false

ERB内のRubyコードの評価メソッド

ERBの中でRubyのコードを書くとそのコードがevalで評価される。

class ERB
def result(b=new_toplevel)
if @safe_level
proc {
$SAFE = @safe_level
eval(@src, b, (@filename || '(erb)'), 0)
}.call
else
eval(@scr, b, (@filename || '(erb)'), 0)
end
end
end

ユーザーが@safe_levelを設定していれば、サンドボックスの中で、コードを評価する。
また、$SAFEはprocの中だけで有効になっており、全体の設定を変更しないようにしている。

🚕 フックメソッド

Rubyにはいくつかのイベントが発生した時にフックするメソッドが存在する。

継承にフック

class String
def self.inherited(subclass)
puts "#{self}#{subclass} に継承されたよ!"
end
end

class MyString < String; end
#=> "String は MyString に継承されたよ!"

includeにフック

module M1
def self.included(othermod)
puts "#{self}#{othermod} にincludeされたよ!"
end
end

class C
include M1
end
#=> M1 は C にincludeされたよ!

prependにフック

module M2
def self.prepended(othermod)
puts "#{self}#{othermod} にprependされたよ!"
end
end

class C
prepend M2
end

#=> M2 は C にprependされたよ!

そのほか以下のようなメソッドもある。

Module#method_added - メソッドを追加した時に呼ばれる
Module#method_removed - メソッドがModule#remove_method により削除された時に呼ばれる
Module#method_undefined - メソッドがModule#undef_method によって削除されるか、 undef 文により未定義になったら呼ばれる

# 特異メソッドのイベントをキャッチする
Kernel#singleton_method_added - 特異メソッドが追加された時に呼ばれる
Kernel#singleton_method_removed - 特異メソッドが削除された時に呼ばれる
Kernel#singleton_method_undefined - 特異メソッドがundefinedになった時に呼ばれる

😸 属性のチェック

全Classで attr_checked を使えるようにする

classやモジュールの属性をチェックするようなDSL attr_checkedを追加する例。

require 'test/unit'

class Class
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' if value.nil? || !value || !validation.call(value)
instance_variable_set("@#{attribute}", value)
end

define_method attribute do
instance_variable_get("@#{attribute}")
end
end
end

class Person
attr_checked :age do |v|
v >= 18
end
end

class TestCheckedAttribute < Test::Unit::TestCase
def setup
@bob = Person.new
end

def test_accepts_valid_values
@bob.age = 18
assert_equal 18, @bob.age
end

def test_refuses_invalid_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = 17
end
end

def test_refuses_nil_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = nil
end
end

def test_refuses_false_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = false
end
end
end

なるほど、これできるのかとちょっと感激!

includeした時だけ使えるようにする

require 'test/unit'

module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end

module ClassMethods
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' if value.nil? || !value || !validation.call(value)
instance_variable_set("@#{attribute}", value)
end

define_method attribute do
instance_variable_get("@#{attribute}")
end
end
end
end

class Person
include CheckedAttributes

attr_checked :age do |v|
v >= 18
end
end

class TestCheckedAttribute < Test::Unit::TestCase
def setup
@bob = Person.new
end

def test_accepts_valid_values
@bob.age = 18
assert_equal 18, @bob.age
end

def test_refuses_invalid_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = 17
end
end

def test_refuses_nil_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = nil
end
end

def test_refuses_false_values
assert_raises RuntimeError, 'Invalid attribute' do
@bob.age = false
end
end
end

🏀 サンプルソース

何かの役に立つこともあるかもですので、リポジトリも公開しておきます。

morizyun/meta_programming_ruby2 - GitHub

🖥 VULTRおすすめ

VULTR」はVPSサーバのサービスです。日本にリージョンがあり、最安は512MBで2.5ドル/月($0.004/時間)で借りることができます。4GBメモリでも月20ドルです。 最近はVULTRのヘビーユーザーになので、「ここ」から会員登録してもらえるとサービス開発が捗ります!

📚 おすすめの書籍