Hey there, I have read the few posts here on when/how to use the visitor pattern, and some articles/chapters on it, and it makes sense if you are traversing an AST and it is highly structured, and you want to encapsulate the logic into a separate "visitor" object, etc. But with Ruby, it seems like overkill because you could just use blocks to do nearly the same thing.
I would like to pretty_print xml using Nokogiri. The author recommended that I use the visitor pattern, which would require I create a FormatVisitor or something similar, so I could just say "node.accept(FormatVisitor.new)".
The issue is, what if I want to start customizing all the stuff in the FormatVisitor (say it allows you to specify how nodes are tabbed, how attributes are sorted, how attributes are spaced, etc.).
One time I want the nodes to have 1 tab for each nest level, and the attributes to be in any order
The next time, I want the nodes to have 2 spaces, and the attributes in alphabetical order
The next time, I want them with 3 spaces and with two attributes per line.
I have a few options:
Create an options hash in the constructor (FormatVisitor.new({:tabs => 2})
Set values after I have constructed the Visitor
Subclass the FormatVisitor for each new implementation
Or just use blocks, not the visitor
Instead of having to construct a FormatVisitor, set values, and pass it to the node.accept method, why not just do this:
node.pretty_print do |format|
format.tabs = 2
format.sort_attributes_by {...}
end
That's in contrast to what I feel like the visitor pattern would look like:
visitor = Class.new(FormatVisitor) do
attr_accessor :format
def pretty_print(node)
# do something with the text
#format.tabs = 2 # two tabs per nest level
#format.sort_attributes_by {...}
end
end.new
doc.children.each do |child|
child.accept(visitor)
end
Maybe I've got the visitor pattern all wrong, but from what I've read about it in ruby, it seems like overkill. What do you think? Either way is fine with me, just wondering what how you guys feel about it.
Thanks a lot,
Lance
In essence, a Ruby block is the Visitor pattern without the extra boilerplate. For trivial cases, a block is sufficient.
For example, if you want to perform a simple operation on an Array object, you would just call the #each method with a block instead of implementing a separate Visitor class.
However, there are advantages in implementing a concrete Visitor pattern under certain cases:
For multiple, similar but complex operations, Visitor pattern provides inheritance and blocks don't.
Cleaner to write a separate test suite for Visitor class.
It's always easier to merge smaller, dumb classes into a larger smart class than separating a complex smart class into smaller dumb classes.
Your implementation seems mildly complex, and Nokogiri expects a Visitor instance that impelment #visit method, so Visitor pattern would actually be a good fit in your particular use case. Here is a class based implementation of the visitor pattern:
FormatVisitor implements the #visit method and uses Formatter subclasses to format each node depending on node types and other conditions.
# FormatVisitor implments the #visit method and uses formatter to format
# each node recursively.
class FormatVistor
attr_reader :io
# Set some initial conditions here.
# Notice that you can specify a class to format attributes here.
def initialize(io, tab: " ", depth: 0, attributes_formatter_class: AttributesFormatter)
#io = io
#tab = tab
#depth = depth
#attributes_formatter_class = attributes_formatter_class
end
# Visitor interface. This is called by Nokogiri node when Node#accept
# is invoked.
def visit(node)
NodeFormatter.format(node, #attributes_formatter_class, self)
end
# helper method to return a string with tabs calculated according to depth
def tabs
#tab * #depth
end
# creates and returns another visitor when going deeper in the AST
def descend
self.class.new(#io, {
tab: #tab,
depth: #depth + 1,
attributes_formatter_class: #attributes_formatter_class
})
end
end
Here the implementation of AttributesFormatter used above.
# This is a very simple attribute formatter that writes all attributes
# in one line in alphabetical order. It's easy to create another formatter
# with the same #initialize and #format interface, and you can then
# change the logic however you want.
class AttributesFormatter
attr_reader :attributes, :io
def initialize(attributes, io)
#attributes, #io = attributes, io
end
def format
return if attributes.empty?
sorted_attribute_keys.each do |key|
io << ' ' << key << '="' << attributes[key] << '"'
end
end
private
def sorted_attribute_keys
attributes.keys.sort
end
end
NodeFormatters uses Factory pattern to instantiate the right formatter for a particular node. In this case I differentiated text node, leaf element node, element node with text, and regular element nodes. Each type has a different formatting requirement. Also note, that this is not complete, e.g. comment nodes are not taken into account.
class NodeFormatter
# convience method to create a formatter using #formatter_for
# factory method, and calls #format to do the formatting.
def self.format(node, attributes_formatter_class, visitor)
formatter_for(node, attributes_formatter_class, visitor).format
end
# This is the factory that creates different formatters
# and use it to format the node
def self.formatter_for(node, attributes_formatter_class, visitor)
formatter_class_for(node).new(node, attributes_formatter_class, visitor)
end
def self.formatter_class_for(node)
case
when text?(node)
Text
when leaf_element?(node)
LeafElement
when element_with_text?(node)
ElementWithText
else
Element
end
end
# Is the node a text node? In Nokogiri a text node contains plain text
def self.text?(node)
node.class == Nokogiri::XML::Text
end
# Is this node an Element node? In Nokogiri an element node is a node
# with a tag, e.g. <img src="foo.png" /> It can also contain a number
# of child nodes
def self.element?(node)
node.class == Nokogiri::XML::Element
end
# Is this node a leaf element node? e.g. <img src="foo.png" />
# Leaf element nodes should be formatted in one line.
def self.leaf_element?(node)
element?(node) && node.children.size == 0
end
# Is this node an element node with a single child as a text node.
# e.g. <p>foobar</p>. We will format this in one line.
def self.element_with_text?(node)
element?(node) && node.children.size == 1 && text?(node.children.first)
end
attr_reader :node, :attributes_formatter_class, :visitor
def initialize(node, attributes_formatter_class, visitor)
#node = node
#visitor = visitor
#attributes_formatter_class = attributes_formatter_class
end
protected
def attribute_formatter
#attribute_formatter ||= #attributes_formatter_class.new(node.attributes, io)
end
def tabs
visitor.tabs
end
def io
visitor.io
end
def leaf?
node.children.empty?
end
def write_tabs
io << tabs
end
def write_children
v = visitor.descend
node.children.each { |child| child.accept(v) }
end
def write_attributes
attribute_formatter.format
end
def write_open_tag
io << '<' << node.name
write_attributes
if leaf?
io << '/>'
else
io << '>'
end
end
def write_close_tag
return if leaf?
io << '</' << node.name << '>'
end
def write_eol
io << "\n"
end
class Element < self
def format
write_tabs
write_open_tag
write_eol
write_children
write_tabs
write_close_tag
write_eol
end
end
class LeafElement < self
def format
write_tabs
write_open_tag
write_eol
end
end
class ElementWithText < self
def format
write_tabs
write_open_tag
io << text
write_close_tag
write_eol
end
private
def text
node.children.first.text
end
end
class Text < self
def format
write_tabs
io << node.text
write_eol
end
end
end
To use this class:
xml = "<root><aliens><alien><name foo=\"bar\">Alf<asdf/></name></alien></aliens></root>"
doc = Nokogiri::XML(xml)
# the FormatVisitor accepts an IO object and writes to it
# as it visits each node, in this case, I pick STDOUT.
# You can also use File IO, Network IO, StringIO, etc.
# As long as it support the #puts method, it will work.
# I'm using the defaults here. ( two spaces, with starting depth at 0 )
visitor = FormatVisitor.new(STDOUT)
# this will allow doc ( the root node ) to call visitor.visit with
# itself. This triggers the visiting of each children recursively
# and contents written to the IO object. ( In this case, it will
# print to STDOUT.
doc.accept(visitor)
# Prints:
# <root>
# <aliens>
# <alien>
# <name foo="bar">
# Alf
# <asdf/>
# </name>
# </alien>
# </aliens>
# </root>
With the above code, you can change node formatting behaviors by constructing extra subclasses of NodeFromatters and plug them into the factory method. You can control the formatting of attributes with various implementation of the AttributesFromatter. As long as you adhere to its interface, you can plug it into the attributes_formatter_class argument without modifying anything else.
List of design patterns used:
Visitor Pattern: handle node traversal logic. ( Also interface requirement by Nokogiri. )
Factory Pattern, used to determine formatter based on node types and other formatting conditions. Note, if you don't like the class methods on NodeFormatter, you can extract them into NodeFormatterFactory to be more proper.
Dependency Injection (DI / IoC), used to control the formatting of attributes.
This demonstrates how you can combine a few patterns together to achieve the flexibility you desire. Although, if you need those flexibility is something you have to decide.
I would go with what is simple and works. I don't know the details, but what you wrote compared with the Visitor pattern, looks simpler. If it also works for you, I would use that. Personally, I am tired with all these techniques that ask you to create a huge "network" of interelated classes, just to solve one small problem.
Some would say, yeah, but if you do it using patterns then you can cover many future needs and blah blah. I say, do now what works and if the need arises, you can refactor in the future. In my projects, that need almost never arises, but that's a different story.
Related
I'm new to coding and new to learning the language Ruby. I've made progress on my code of a letter histogram. I'm now at a point where I'm struggling.
class Letter
def initialize
#text = "Hello, World!"
#letters = Hash.new(0)
end
def calculateFrequencies
#text = text.downcase.chars.each do |c|
next if c =~ /\s/
#letters[c] += 1
end
puts(#letters)
end
def Display
end
end
I have created an application that will accept a user input message, process it and output a list of letters with a number to represent how often that letter appeared in the user's message.
I would like the output to list the entire alphabet, and use * to represent how often that letter was used.
For example:
A: **** (For being used 4 times)
B: ** (For being used 2 times)
C: (Even if it wasn't used)
D: * (For being used once) ... and so on all the way to letter Z.
This is my current output for "This is a test":
irb(main):019:0> h.calculateFrequency
{"t"=>3, "h"=>1, "i"=>2, "s"=>3, "a"=>1, "e"=>1}
I suggest you construct your class as follows.
class Letter
def initialize(text) # 1
#text = text
end
def display
h = calculate_frequencies # 2
('a'..'z').each { |ltr|
puts "%s%s" % [ltr, '*' * h.fetch(ltr, 0)] } # 3
end
private # 4
def calculate_frequencies # 5
#text.downcase.
each_char. # 6
with_object(Hash.new(0)) { |c, letters| # 7
letters[c] += 1 if c.match?(/\p{Lower}/) } # 8
end
end
str = "Now is the time for all Rubyists to come to the aid of their bowling team."
ltr = Letter.new(str)
ltr.display
a***
b**
c*
d*
e******
f**
g*
h***
i******
j
k
l***
m***
n**
o*******
p
q
r***
s***
t********
u*
v
w**
x
y*
z
Notes
1
text should be an argument of initialize so the methods can be used for any string, rather than for just one hard-wired string. #letters should not be initialized here as it needs to be initialized in calculate_frequencies each time that method is called (and there it need not be an instance variable).
2
For str, calculate_frequencies returns
ltr.send(:calculate_frequencies)
#=> {"n"=>2, "o"=>7, "w"=>2, "i"=>6, "s"=>3, "t"=>8, "h"=>3, "e"=>6,
# "m"=>3, "f"=>2, "r"=>3, "a"=>3, "l"=>3, "u"=>1, "b"=>2, "y"=>1,
# "c"=>1, "d"=>1, "g"=>1}
Object#send invokes private methods, as well as ones that are public or protected.
3
See Hash#fetch and String#*.
4
All methods defined after the invocation of the keyword private are private, until and if the keyword public or protected is encountered. Alternatively, one can define a single private method as private def calculate_frequencies; ... ; end. Also a public (or protected) method m may be made private by executing private m.
5
One of Ruby's conventions is to use snake-case for names of variables and methods. You don't have to follow that convention but 99%+ of Rubyists do.
6
String#each_char returns an enumerator, whereas String#chars returns an array. The latter should only be used when an array is needed or it is chained to an Array method; otherwise, each_char is preferable because it does not create an unneeded temporary array.
7
See Enumerator#with_object.
8
Rather than matching everything other than spaces, you probably want to only match letters. Note how I've used if here to avoid the need for two statements. See String#match?. One could instead write c =~ /\p{Lower}/ or c[/\p{Lower}/]. \p{Lower} (or [[:lower:]]) matches any Unicode lower-case letter, which generally is preferable to /[a-z]/. Even for English text, one may encounter words having letters with diacritical marks, such as née, Señor, exposé and rosé. "é".match? /[a-z]/ #=> false but "é".match? /\p{Lower}/ #=> true. Search the doc Regexp for \p{Lower} and [[:lower:]].
Good start :)
In order to make a method private, place it after a call to private in the class:
class foo
# all methods are public by default
def something_public
# …
end
private
# makes all methods after this call private
def something_internal
# …
end
end
Alternatively, you can call private with the symbol name of the method you want to make private (after it has been defined): private :something_internal. In newer versions of ruby defining a method returns the method name (as a symbol), so you can also do this:
private def something_internal
# …
end
to make just the one method private.
In ruby, "private" means that you can't call the method with a dot, e.g. foo.something_internal will raise a NoMethodError if something_internal is a private method on foo. That means that to call a private method, you need to be in a method in the same class:
class foo
# …
def something_public
if something_internal # method called without a dot
'the internal check was truth'
else
'the internal check was falsey'
end
end
# …
end
Private methods are usually used to create helpers for the class that don't make sense to be called from outside the class, or that can cause bugs if they are called at the wrong time. In ruby, you can call a private method anyway if you really want by using send: foo.send(:something_internal, 'some', 'arguments'). But generally, you shouldn't need to, and you should rethink your code and see if you can refactor it to not need to call send.
Also, by convention in ruby, method names are snake_cased and usually don't start with a capital (although the language allows this).
I have an array to which I keep adding blocks of code at different points of time. When a particular event occurs, an iterator iterates through this array and yields the blocks one after the other.
Many of these blocks are the same and I want to avoid executing duplicate blocks.
This is sample code:
#after_event_hooks = []
def add_after_event_hook(&block)
#after_event_hooks << block
end
Something like #after_event_hooks.uniq or #after_event_hooks |= block don't work.
Is there a way to compare blocks or check their uniqueness?
The blocks can not be checked for uniqueness since that will mean to check whether they represent the same functions, something that is not possible and has been researched in computer science for a long time.
You can probably use a function similar to the discussed in "Ruby block to string instead of executing", which is a function that takes a block and returns a string representation of the code in the block, and compare the output of the strings you receive.
I am not sure if this is fast enough to be worthy to compare them, instead of executing them multiple times. This also has the downside you need to be sure the code is exactly the same, even one variable with different name will break it.
As #hakcho has said, it is not trivial to compare blocks. A simple solution might be having the API request for named hooks, so you can compare the names:
#after_event_hooks = {}
def add_after_event_hook(name, &block)
#after_event_hooks[name] = block
end
def after_event_hooks
#after_event_hooks.values
end
Maybe use something like this:
class AfterEvents
attr_reader :hooks
def initialize
#hooks = {}
end
def method_missing(hook_sym, &block)
#hooks[hook_sym] = block
end
end
Here is a sample:
events = AfterEvents.new
events.foo { puts "Event Foo" }
events.bar { puts "Event Bar" }
# test
process = {:first => [:foo], :sec => [:bar], :all => [:foo, :bar]}
process.each { |event_sym, event_codes|
puts "Processing event #{event_sym}"
event_codes.each { |code| events.hooks[code].call }
}
# results:
# Processing event first
# Event Foo
# Processing event sec
# Event Bar
# Processing event all
# Event Foo
# Event Bar
I want to design a class that behaves like an array, but also allows me to separate one set of data from another set so that certain operations only act on data from the other set. What is a good way to do this?
My design is to have initial set of data stored in one array, and any new elements that are added to this object are tracked separately (called "extra" data) in a different array.
Adding and deleting items from objects of this class will operate on the new elements only, while any sort of search operations will be performed on the combination of the initial elements as well as the new elements.
An element's index in this object is the element's index in the concatenation of the two arrays, so if there are 3 elements in the initial data and 4 elements in the extra array, then the first element in the extra array will return an index of 3, while the first element of the initial array will return an index of 0.
For illustration purposes this is how it might look in code
class MyClass
def initialize(base_data)
#base_elements = base_data # this is an array
#extra_elements = [] # this stores the "new" data
end
def add(elem)
#extra_elements << elem
end
def delete(elem)
#extra_elements << elem
end
def all_elements
#base_elements.concat(#extra_elements)
end
def find(elem)
all_elements.find( ... )
end
def index (elem)
all_elements.index( ... )
end
end
Ideally, this should support all of the methods defined in Enumerator as well, such as each, inject, etc.
You are pretty much there based on what you are asking for just a couple modifications:
class MyClass
include Enumerable
def initialize(base_data)
#base_elements = base_data # this is an array
#extra_elements = [] # this stores the "new" data
end
def add(elem)
#extra_elements << elem
end
def delete(elem)
#extra_elements.delete(elem)
end
def all_elements
#base_elements.concat(#extra_elements)
end
def index (elem)
all_elements.index( ... )
end
def each(&block)
all_elements.each(&block)
end
end
I am looking into parslet to write alot of data import code. Overall, the library looks good, but I'm struggling with one thing. Alot of our input files are fixed width, and the widths differ between formats, even if the actual field doesn't. For example, we might get a file that has a 9-character currency, and another that has 11-characters (or whatever). Does anyone know how to define a fixed width constraint on a parslet atom?
Ideally, I would like to be able to define an atom that understands currency (with optional dollar signs, thousand separators, etc...) And then I would be able to, on the fly, create a new atom based on the old one that is exactly equivalent, except that it parses exactly N characters.
Does such a combinator exist in parslet? If not, would it be possible/difficult to write one myself?
What about something like this...
class MyParser < Parslet::Parser
def initialize(widths)
#widths = widths
super
end
rule(:currency) {...}
rule(:fixed_c) {currency.fixed(#widths[:currency])}
rule(:fixed_str) {str("bob").fixed(4)}
end
puts MyParser.new.fixed_str.parse("bob").inspect
This will fail with:
"Expected 'bob' to be 4 long at line 1 char 1"
Here's how you do it:
require 'parslet'
class Parslet::Atoms::FixedLength < Parslet::Atoms::Base
attr_reader :len, :parslet
def initialize(parslet, len, tag=:length)
super()
raise ArgumentError,
"Asking for zero length of a parslet. (#{parslet.inspect} length #{len})" \
if len == 0
#parslet = parslet
#len = len
#tag = tag
#error_msgs = {
:lenrep => "Expected #{parslet.inspect} to be #{len} long",
:unconsumed => "Extra input after last repetition"
}
end
def try(source, context, consume_all)
start_pos = source.pos
success, value = parslet.apply(source, context, false)
return succ(value) if success && value.str.length == #len
context.err_at(
self,
source,
#error_msgs[:lenrep],
start_pos,
[value])
end
precedence REPETITION
def to_s_inner(prec)
parslet.to_s(prec) + "{len:#{#len}}"
end
end
module Parslet::Atoms::DSL
def fixed(len)
Parslet::Atoms::FixedLength.new(self, len)
end
end
Methods in parser classes are basically generators for parslet atoms. The simplest form these methods come in are 'rule's, methods that just return the same atoms every time they are called. It is just as easy to create your own generators that are not such simple beasts. Please look at http://kschiess.github.com/parslet/tricks.html for an illustration of this trick (Matching strings case insensitive).
It seems to me that your currency parser is a parser with only a few parameters and that you could probably create a method (def ... end) that returns currency parsers tailored to your liking. Maybe even use initialize and constructor arguments? (ie: MoneyParser.new(4,5))
For more help, please address your questions to the mailing list. Such questions are often easier to answer if you illustrate it with code.
Maybe my partial solution will help to clarify what I meant in the question.
Let's say you have a somewhat non-trivial parser:
class MyParser < Parslet::Parser
rule(:dollars) {
match('[0-9]').repeat(1).as(:dollars)
}
rule(:comma_separated_dollars) {
match('[0-9]').repeat(1, 3).as(:dollars) >> ( match(',') >> match('[0-9]').repeat(3, 3).as(:dollars) ).repeat(1)
}
rule(:cents) {
match('[0-9]').repeat(2, 2).as(:cents)
}
rule(:currency) {
(str('$') >> (comma_separated_dollars | dollars) >> str('.') >> cents).as(:currency)
# order is important in (comma_separated_dollars | dollars)
}
end
Now if we want to parse a fixed-width Currency string; this isn't the easiest thing to do. Of course, you could figure out exactly how to express the repeat expressions in terms of the final width, but it gets really unnecessarily tricky, especially in the comma separated case. Also, in my use case, currency is really just one example. I want to be able to have an easy way to come up with fixed-width definitions for adresses, zip codes, etc....
This seems like something that should be handle-able by a PEG. I managed to write a prototype version, using Lookahead as a template:
class FixedWidth < Parslet::Atoms::Base
attr_reader :bound_parslet
attr_reader :width
def initialize(width, bound_parslet) # :nodoc:
super()
#width = width
#bound_parslet = bound_parslet
#error_msgs = {
:premature => "Premature end of input (expected #{width} characters)",
:failed => "Failed fixed width",
}
end
def try(source, context) # :nodoc:
pos = source.pos
teststring = source.read(width).to_s
if (not teststring) || teststring.size != width
return error(source, #error_msgs[:premature]) #if not teststring && teststring.size == width
end
fakesource = Parslet::Source.new(teststring)
value = bound_parslet.apply(fakesource, context)
return value if not value.error?
source.pos = pos
return error(source, #error_msgs[:failed])
end
def to_s_inner(prec) # :nodoc:
"FIXED-WIDTH(#{width}, #{bound_parslet.to_s(prec)})"
end
def error_tree # :nodoc:
Parslet::ErrorTree.new(self, bound_parslet.error_tree)
end
end
# now we can easily define a fixed-width currency rule:
class SHPParser
rule(:currency15) {
FixedWidth.new(15, currency >> str(' ').repeat)
}
end
Of course, this is a pretty hacked solution. Among other things, line numbers and error messages are not good inside of a fixed width constraint. I would love to see this idea implemented in a better fashion.
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:
These internal methods are somewhat computationally-intensive recursive functions, and I don't want the overhead of checking multiple times at every step
There is no way, without adding an extra parameter, to tell whether or not to re-convert at the end
I want to think of this as an exercise in understanding ruby meta-programming
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.
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.