欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ruby元编程实际使用实例

程序员文章站 2022-04-28 17:54:36
很喜欢ruby元编程,puppet和chef用到了很多ruby的语言特性,来定义一个新的部署语言。 分享几个在实际项目中用到的场景,能力有限,如果有更优方案,请留言给我:...

很喜欢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("\n")
   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}\n"}
     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

上面这个小技巧使得templatedsl对象可以应用block,和在自己的scope一样。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 "\n#{@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