rubyメタプログラミングの実際の使用例

6575 ワード

rubyメタプログラミングが大好きで、puppetとchefは多くのrubyの言語特性を使って、新しい配置言語を定義します.実際のプロジェクトで使われているシーンをいくつか共有します.能力が限られています.もっと優れた案があれば、私にメッセージを残してください.)
rpcインタフェースのテンプレート化――eval、alias、defind_の使用method

require 'rack/rpc'

class Server < Rack::RPC::Server
 def hello_world
  "Hello, world!"
 end

 rpc 'hello_world' => :hello_world
end

上にはrpc serverがあり、関数を作成し、rpcコマンドを呼び出して登録します.
define_を採用method、eval、aliasメソッドは、rpc/ディレクトリの下を判断する*を実現することができる.rbファイルは、ロードとrpcインタフェースの登録を行う機能で、実現コードは以下の通りです.

module RPC
  require 'rack/rpc'
  #require rpc/*.rb  
  Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file|
   require file
  end
  class Runner < Rack::RPC::Server
   #include rpc/*.rb and regsiter rpc call
   #eg. rpc/god.rb  god.hello
   @@rpc_list = []
   Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file|
    rpc_class = File.basename(file).split('.rb')[0].capitalize
    rpc_list = []
    
    #  module     Runner     
    eval "include Frigga::RPC::#{rpc_class}"
    #     RPC  
    eval "rpc_list = Frigga::RPC::#{rpc_class}::RPC_LIST"
    rpc_list.each do |rpc_name|
     #alias    rpc  , old_xxxx_xxxx
     eval "alias :old_#{rpc_class.downcase}_#{rpc_name} :#{rpc_name}"

     #    rpc  ,          ,     old_xxxx_xxxx rpc  
     define_method "#{rpc_class.downcase}_#{rpc_name}".to_sym do |*arg|
      Logger.info "[#{request.ip}] called #{rpc_class.downcase}.#{rpc_name} #{arg.join(', ')}"
      eval "old_#{rpc_class.downcase}_#{rpc_name} *arg"
     end 

     #  RPC  
     rpc "#{rpc_class.downcase}.#{rpc_name}" => "#{rpc_class.downcase}_#{rpc_name}".to_sym

     #       ,     rpc  
     @@rpc_list << "#{rpc_class.downcase}.#{rpc_name}"
    end
   end
   
   def help
    rpc_methods = (['help'] + @@rpc_list.sort).join("
") end rpc "help" => :help end end #RPC

上記の機能が完了すると、rpcインタフェースの開発が非常に便利になり、例えば以下のIPアドレスの増加、削除、検索のコード、ipの登録が可能になる.list, ip.addとip.delメソッド:

module RPC
  module Ip
   #RPC_LIST used for regsiter rpc_call
   RPC_LIST = %w(list add del)

   def list
    $white_lists
   end   

   def add(ip) 
    if ip =~ /^((25[0-5]|2[0-4]\d|[0-1]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[0-1]?\d\d?)$/
     $white_lists << ip
     write_to_file
     return "succ"
    else
     return "fail"
    end
   end

   def del(ip)
    if $white_lists.include?(ip)
     $white_lists.delete ip
     write_to_file
     return "succ"
    else
     return "fail"
    end    
   end

   def write_to_file
     File.open(IP_yml, "w") do |f|
      $white_lists.uniq.each {|i| f << "- #{i}
"} end end end end

DSL――instance_の使用eval
instance_evalはruby言語のスイスの軍刀であり、特にDSL側をサポートしている.chef(オープンソースの自動化導入ツール)でファイルテンプレートを設定するAPIを見てみましょう.
 
  
    template "/path/to/file.conf" do
      source "file.conf.erb"
      owner  "wilbur"
      mode   "0744"
    end

上記コードでは、source、owner、modeは外部blockからtemplate内部のblockに渡す必要があり、その目的を達成するためにinstance_を採用しているevalコードは次のとおりです.

  class ChefDSL
   def template(path, &block)
    TemplateDSL.new(path, &block)
   end
  end

  class TemplateDSL
   def initialize(path, &block)
    @path = path
    instance_eval &block
   end

   def source(source); @source = source; end
   def owner(owner);  @owner = owner; end
   def mode(mode);   @mode  = mode;  end
  end

上の小さなテクニックは、自分のscopeと同じように、TemplateDSLオブジェクトにblockを適用することができます.blockはTemplateDSLの変数とメソッドにアクセスして呼び出すことができます.
instanceを使用していない場合evalは、次のコードのようにrubyがNoMethodErrorを投げ出す.source、owner、modeはblockにアクセスできないからだ.
 
  
    class TemplateDSL
      def initialize(path, &block)
        @path = path
        block.call
      end
    end

もちろんyeildで変数を渡す方式で実現することもできますがinstance_はありませんevalは簡潔で柔軟です.
コマンドラインインタラクション――instance_の使用eval
コマンドラインのインタラクションはhighlineというgemを採用することができる.しかし、highlineはいくつかの面で私のニーズを満たすことができません.例えば、上記のchef template機能のように、以下のような効果を達成し、重複コードを大幅に簡略化しました.
 
  
        # frigga fail,
        Tip.ask frigga_fail? do
          banner "Check some frigga failed, skip failed host and continue deploy?"
          on :yes
          on :quit do
            raise Odin::TipQuitExcption
          end
        end
        ...

        # :
        Check some frigga failed, skip failed host and continue deploy? [yes/quit]
        # yes , quit


実装コードは次のとおりです.

 require 'colorize'
 class Tip
  def self.ask(stat = true, &block)
   new(&block).ret if stat == true
  end

  attr_reader :ret
  def initialize(&block)
   @opt = []
   @caller = {}
   @banner = ""
   @ret = false
   self.instance_eval(&block)
   print "#{@banner} [#{@opt.join('/')}]: ".light_yellow
   loop do
    x = gets.chomp.strip.to_sym
    if @opt.include?(x)
     @ret = ( @caller[x].call if @caller.key?(x) )
     if @ret == :retry
      print "
#{@banner} [#{@opt.join('/')}]: ".light_yellow next else return @ret end else print "input error, please enter [#{@opt.join('/')}]: ".light_yellow end end end def on(opt, &block) @opt << opt @caller[opt] = block if block_given? end def banner(str) @banner = str end end