Reputation: 13600
I've multiply heard Ruby touted for its super spectacular meta-programming capabilities, and I was wondering if anyone could help me get started with this problem.
I have a class that works as an "archive" of sorts, with internal methods that process and output data based on an input. However, the items in the archive in the class itself are represented and processed with integers, for performance purposes. The actual items outside of the archive are known by their string representation, which is simply number_representation.to_s(36).
Because of this, I have hooked up each internal method with a "proxy method" that converts the input into the integer form that the archive recognizes, runs the internal method, and converts the output (either a single other item, or a collection of them) back into strings.
The naming convention is this: internal methods are represented by _method_name; their corresponding proxy method is represented by method_name, with no leading underscore.
For example:
class Archive
## PROXY METHODS ##
## input: string representation of id's
## output: string representation of id's
def do_something_with id
result = _do_something_with id.to_i(36)
return nil if result == nil
return result.to_s(36)
end
def do_something_with_pair id_1,id_2
result = _do_something_with_pair id_1.to_i(36), id_2.to_i(36)
return nil if result == nil
return result.to_s(36)
end
def do_something_with_these ids
result = _do_something_with_these ids.map { |n| n.to_i(36) }
return nil if result == nil
return result.to_s(36)
end
def get_many_from id
result = _get_many_from id
return nil if result == nil # no sparse arrays returned
return result.map { |n| n.to_s(36) }
end
## INTERNAL METHODS ##
## input: integer representation of id's
## output: integer representation of id's
private
def _do_something_with id
# does something with one integer-represented id,
# returning an id represented as an integer
end
def do_something_with_pair id_1,id_2
# does something with two integer-represented id's,
# returning an id represented as an integer
end
def _do_something_with_these ids
# does something with multiple integer ids,
# returning an id represented as an integer
end
def _get_many_from id
# does something with one integer-represented id,
# returns a collection of id's represented as integers
end
end
There are a couple of reasons why I can't just convert them if id.class == String at the beginning of the internal methods:
Does anyone have any ideas?
edit
The solution I'd like would preferably be able to take an array of method names
@@PROXY_METHODS = [:do_something_with, :do_something_with_pair,
:do_something_with_these, :get_many_from]
iterate through them, and in each iteration, put out the proxy method. I'm not sure what would be done with the arguments, but is there a way to test for arguments of a method? If not, then simple duck typing/analogous concept would do as well.
I've come up with my own solution, using #class_eval
@@PROXY_METHODS.each do |proxy|
class_eval %{ def #{proxy} *args
args.map! do |a|
if a.class == String
a.to_i(36)
else
a.map { |id| id.to_i(36) }
end
end
result = _#{proxy}(*args)
result and if result.respond_to?(:each)
result.map { |r| r.to_s(36) }
else
result.to_s(36)
end
end
}
end
However, #class_eval
seems a bit...messy? or inelegant compared to what it "should" be.
Upvotes: 1
Views: 918
Reputation: 89063
class Archive
# define a new method-creating method for Archive by opening the
# singleton class for Archive
class << Archive
private # (make it private so no one can call Archive.def_api_method)
def def_api_method name, &defn
define_method(name) do |*args|
# map the arguments to their integer equivalents,
# and pass them to the method definition
res = defn[ *args.map { |a| a.to_i(36) } ]
# if we got back a non-nil response,
res and if res.respond_to?(:each)
# map all of the results if many returned
res.map { |r| r.to_s(36) }
else
# map the only result if only one returned
res.to_s(36)
end
end
end
end
def_api_method("do_something_with"){ |id| _do_something_with(id) }
def_api_method("do_something_with_pair"){ |id_1, id_2| _do_something_with_pair id_1.to_i(36), id_2.to_i(36) }
#...
end
Instead of opening the singleton to define Archive.def_api_method
, you could define it simply using
class Archive
def Archive.def_api_method
#...
But the reason I didn't do that is then anyone with access to the Archive
class could invoke it using Archive.def_api_method
. Opening up the singleton class allowed me to mark def_api_method
as private, so it can only be invoked when self == Archive
.
If you're always going to be calling an internal version with the same (or derivable) name, then you could just invoke it directly (rather than pass a definition block) using #send
.
class Archive
# define a method-creating method that wraps an internal method for external use
class << Archive
private # (make it private so no one can call Archive.api_method)
def api_method private_name
public_name = private_name.to_s.sub(/^_/,'').to_sym
define_method(public_name) do |*args|
# map the arguments to their integer equivalents,
# and pass them to the private method
res = self.send(private_name, *args.map { |a| a.to_i(36) })
# if we got back a non-nil response,
res and if res.respond_to?(:each)
# map all of the results if many returned
res.map { |r| r.to_s(36) }
else
# map the only result if only one returned
res.to_s(36)
end end
# make sure the public method is publicly available
public public_name
end
end
api_method :_do_something_with
api_method :_do_something_with_pair
private
def _do_something_with
#...
end
def _do_something_with_pair
#...
end
end
This is more like what is done by other meta-methods like attr_reader
and attr_writer
.
Upvotes: 2