メタプログラミング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
# 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

📚 おすすめの書籍

🖥 サーバについて

このブログでは「Cloud Garage」さんのDev Assist Program(開発者向けインスタンス無償提供制度)でお借りしたサーバで技術検証しています。 Dev Assist Programは、開発者や開発コミュニティ、スタートアップ企業の方が1GBメモリのインスタンス3台を1年間無料で借りれる心強い制度です!(有償でも1,480円/月と格安)