読者です 読者をやめる 読者になる 読者になる

気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

メタプログラミング Ruby 第5章 コードを記述するコード

コードを記述するコード。一般的に言われているメタプログラミングの概念。
メタプログラミングの総集編の章。動的メソッドカーネルメソッドなどの復習をしつつ、
フックメソッドなどの新しいトリックを理解していきます。

evalの問題点

evalが評価するコード文字列内でもローカル変数にアクセスできる。
そういう意味ではブロックと似ている。

v1 = 1
eval("v1") # => 1

evalとブロックどちらを使うのが良いか。
答えは「ブロック」らしい。 コード文字列(evalを用いた処理記述)は問題点がある。

コードインジェクション
外部からコード文字列を読み込んだ際に、悪意のあるコードを読み込んでしまい、
プライベート情報漏洩やハードディスクの情報を消されたりしてしまうこと

def my_method(method)
  eval("'a'.#{method}")
end

# 悪意のあるコード プライベートな情報が表示されてしまう。
my_method(object_id; Dir.glob("*")

'a'.object_id
Dir.glob("*")

オブジェクトの汚染とセーフレベル

Rubyは安全でないオブジェクトに汚染マークを付ける。
webフォーム、コマンドライン、システム変数の読み込み文字列など
を含んでいた場合、安全でないと判断

user_input = "User input: #{gets()}"
# 汚染されているかどうかをチェック
puts user_input.tainted?

x = 1
true

汚染マークを検知して、例外を発生させるセーフレベルという仕組みがある
潜在的に危険な操作に対して制限をかけられる。

# セーフレベルを1に設定
user_input = "User input: #{gets()}"
eval user_input

x = 1
# セーフレベル1以上の場合、汚染した文字列を評価できない
SecurityError: Insecure operatiion - eval

セーフレベルについては以下を参照
セキュリティモデル (Ruby 1.8.7)

属性追加メソッドを作成(evalを使用)

コードを記述するコードについて学ぶ為に
attr_accessor()に検証機能を付加したadd_checked_attribute()を作成する。

特徴は以下
カーネルメソッド
Ruby(コード文字列)を使用
・シンプルな検証機能

まず、満たしたい機能のテストを書く。

require 'test/unit'

class Person; end

class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    # Personクラスにage属性と取得、設定メソッドを追加
    add_checked_attribute(Person, :age)
    @bob = Person.new
  end

  def test_accepts_vaid_value
    @bob.age = 20
    assert_equal 20, @bob.age
  end

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

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

  def add_checked_attribute(clazz, attribute)
    # この後実装
  end
end

add_checked_attributeをevalを用いて実装

def add_checked_attribute(clazz, attribute)
  eval ""
    # オープンクラスを用いて、指定クラスにメソッド追加
    class #{clazz}
      def #{attribute}=(value)
        raise 'Invalid attribute' unless value
        @#{attribute} = value
      end

      def #{attribute}()
        @#{attribute}
      end
    end
  ""
end

# 上記メソッドを呼び出した場合
add_checked_attribute(Array, my_attr)
class Array
  def my_attr=(value)
    raise 'Invalid attribute' unless value
    @my_attr = value
  end

  def my_attr()
    @my_attr
  end
end

属性追加メソッドを作成(evalを使わないで実装)

add_checked_attributeが外部公開されるとすると、 コードインジェクション対策としてevalを使わない実装にリファクタリングする必要がある。

def add_checked_attribute(clazz, attribute)
  # 任意のクラスのクラススコープにメソッドを定義
  clazz.class_eval do
    # 任意の名称のメソッドを定義するので動的メソッドを使う
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless value
      # 任意のインスタンス変数に値を設定するのでinstance_variable_setを使う
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      # 任意のインスタンス変数の値を取得するのでinstance_variable_setを使う
      instance_variable_get "@#{attribute}"
    end
  end
end

属性追加メソッドを作成(属性検証にブロック使用可とする)

ここでは設定値が18以上でないと例外発生する条件をブロック指定する。
上記、仕様追加のため、テストコードを追加

require 'test/unit'

class Person; end

class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    # Personクラスにage属性と取得、設定メソッドを追加
    add_checked_attribute(Person, :age) {|v| v >= 18 }
    @bob = Person.new
  end

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

  def add_checked_attribute(clazz, attribute, &validation)
    clazz.class_eval do
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' unless validation.call(value)
        instance_variable_set("@#{attribute}", value)
      end

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

属性追加メソッドを作成(カーネルメソッドをクラスマクロへ)

require 'test/unit'

class Person
  # クラスマクロとして呼び出す
  attr_checked :age do |v|
    v >= 18
  end
end

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

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

class Class
  # Classのインスタンスメソッドとして定義。Personクラスではクラスメソッドになる
  def attr_checked(attribute, &validation)
    # Class自身に実装されるのでclass_eval不要になった    
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

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

フックメソッド

さまざまなイベントを契機に処理を行うことができる。(継承、拡張など)

module MyModule
  # includedがフックメソッド 
  def self.included(othermod)
    # MyModuleが拡張されると下記が処理される
    puts "MyModuleは #{othermod}にmixinされた"
  end
end

class C
  include MyModule
end

# => MymoduleはCにmixinされた

属性追加メソッドを作成(モジュール拡張時に自動でメソッド追加)

CheckedAttributesをインクルードしたクラスのみが、
attr_checkedにアクセスできるように修正する。

require 'test/unit'

class Person
  include CheckedAttributes 

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

・・・

CheckedAttributesモジュールを実装する。
クラス拡張ミックスインのトリックを使う。include時にクラス拡張を行う。

module CheckedAttributes
  # included呼び出し先クラス(base)のクラスメソッドとしてattr_checkedを定義
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
   def attr_checked(attribute, &validation)
   ・・・

第5章完了