Ruby metaprogramming examples
I like ruby metaprogramming very much. puppet And chef use many ruby language features to define a new deployment language.
I would like to share some of the scenarios used in actual projects with limited capabilities. If you have a better solution, please leave a message to me :)
Rpc interface templated-use eval, alias, defind_method
require 'rack/rpc'
class Server < Rack::RPC::Server
def hello_world
"Hello, world!"
end
rpc 'hello_world' => :hello_world
end
The above is an rpc server. Compile a function and call the rpc command for registration.
The define_method, eval, and alias methods can be used to determine the *. rb file under the rpc/directory for loading and rpc interface registration. The implementation code is as follows:
module RPC
require 'rack / rpc'
#require rpc / *. rb files
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 = []
#Load the methods under module to the Runner class
eval "include Frigga :: RPC :: # {rpc_class}"
#Get declared RPC interface
eval "rpc_list = Frigga :: RPC :: # {rpc_class} :: RPC_LIST"
rpc_list.each do | rpc_name |
#aliasA new rpc method called old_xxxx_xxxx
eval "alias: old _ # {rpc_class.downcase} _ # {rpc_name}: # {rpc_name}"
#Redefine rpc method, add a line of log printing function, and then call 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
#Register RPC call
rpc "# {rpc_class.downcase}. # {rpc_name}" => "# {rpc_class.downcase} _ # {rpc_name}". to_sym
#Add to global variables, summarize all rpc methods
@@ 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
After completing the above functions, you can easily develop the rpc interface, such as the following code for adding, deleting, and querying ip addresses, registering ip. list, ip. add, and ip. del methods:
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 -- use instance_eval
Instance_eval is a Swiss Army knife in ruby, especially in DSL support.
Let's take a look at the API for setting file templates in chef (an open-source automated deployment tool:
Copy codeThe Code is as follows:
Template "/path/to/file. conf" do
Source "file. conf. erb"
Owner "wilbur"
Mode "0744"
End
In the above Code, the source, owner, and mode must be transferred from the external block to the block inside the template. To achieve this purpose, the instance_eval code is used as follows:
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
The above tips enable the TemplateDSL object to apply block, just like in its own scope. Block can access and call variables and methods in TemplateDSL.
If instance_eval is not used, ruby will throw a NoMethodError because source, owner, and mode cannot be accessed in the block.
Copy codeThe Code is as follows:
Class TemplateDSL
Def initialize (path, & block)
@ Path = path
Block. call
End
End
You can also use yeild to pass variables, but instance_eval is not concise and flexible.
Command Line interaction-use instance_eval
Command Line interaction. You can use the gem highline.
However, highline cannot meet my needs in some aspects. For example, the chef template function described above has achieved the following results, greatly simplifying repeated code:
Copy codeThe Code is as follows:
# Check frigga fail and ask whether to continue
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
...
# The running result is as follows:
Check some frigga failed, skip failed host and continue deploy? [Yes/quit]
# Enter yes to continue. Enter quit to exit.
The implementation code is as follows:
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
What's the next project
Welcome! Welcome! Welcome! Welcome! Welcome! Welcome!
Error Message