Recursive send

C

Curtis Summers

I posted this to the Dallas Ruby Brigade, but thought it might be
interesting for the larger Ruby audience. Your comments, questions,
and complete debunking of why something like this is even necessary are
welcomed.

Curtis Summers
---------

Ruby's Object#send is very useful, but what if we wanted to call
several levels deep on an object? For instance:

# Normal call chain
post.comments.first.commented_at

# Dynamically with send? Have to call three times.
post.send:)comments).send:)first).send:)commented_at)


What if the number of calls to send is variable depending on what we're
trying to show? In one case we might need post.posted_at for the date,
and in another case we might need post.comments.first.commented_at for
the date.

How could we dynamically craft the definition of the methods to send if
we don't know how many calls to Object#send we'll have? We need a way
to define an arbitrary number of method calls.

Behold, a recursive send: Object#rsend

class Object
def rsend(*args, &block)
obj = self
args.each do |a|
b = (a.is_a?(Array) && a.last.is_a?(Proc) ? a.pop : block)
obj = obj.__send__(*a, &b)
end
obj
end
alias_method :__rsend__, :rsend
end

Each argument passed to Object#rsend is an array with the symbols and
arguments that will be passed on to Object#send:

post.rsend([:comments],[:first],[:commented_at])

If there are no arguments to be passed on to send, the array brackets
can be omitted:

post.rsend:)comments, :first, :commented_at)


Of course, in practice you'll probably be defining your method call
chain in one part of your code, putting it in a variable, and sending
it to rsend with a splat*:

the_date = [:comments, :first, :commented_at]

#...somewhere else in your code you've passed the_date along:
post.rsend(*the_date)


With arguments:

a = [0,1,2,3,4,5,6,7,8,9]

a.rsend([:slice, 2, 8]) #=> [2, 3, 4, 5, 6, 7, 8, 9]

a.rsend([:slice, 2, 8], [:slice, 1, 3]) #=> [3, 4, 5]


Object#send accepts a block. What about blocks? Pass in a proc:

a.rsend([:map, (proc { |x| x*2 })])
#=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

a.rsend([:map, (proc { |x| x*2 })],
[:select, (proc { |x| x % 4 == 0})])
#=> [0, 4, 8, 12, 16]

And, in an effort to make Object#rsend behave like Object#send for the
simple case, you can send a regular block:

a.rsend:)map) { |x| x*2 }
#=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Caveat: For the case needing parameters, Object#rsend does require an
array, so:

a.rsend:)slice, 2, 8) # wrong, does not work like Object#send

a.rsend([:slice, 2, 8]) # right

A quirk that I've left in for fun, but it might (and maybe should)
change: If providing a single block, that block will be called on
every call unless you've already passed in a proc:

a.rsend:)map, :map) { |x| x*2 }
#=> [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]

a.rsend:)map, [:map, (proc { |x| x+5 })], :map) { |x| x*2 }
#=> [10, 14, 18, 22, 26, 30, 34, 38, 42, 46]
#outer block was called on first and third :map

Can anyone come up with a good use for this call-the-block-each-time
behavior?

Has anyone done this already? I searched for such a thing and came up
empty. Maybe this method should be called something else? I named it
based on each call recursing down the chain of methods with a new
object being returned for the next method to be sent to.

Suggestions and comments are welcome.
 
D

Daniel DeLorme

Curtis said:
# Dynamically with send? Have to call three times.
post.send:)comments).send:)first).send:)commented_at)


What if the number of calls to send is variable depending on what we're
trying to show? In one case we might need post.posted_at for the date,
and in another case we might need post.comments.first.commented_at for
the date.

A word of warning: I once thought of using that same functionality in order
to post complex information from an html form, e.g.
<textarea name="blog[text]"></textarea>
<select name="blog[text.format]">
<option>raw html</option>
<option>textile</option>

But I then realized that was a major security hole. It allows an attacker
to post stuff like:
<input name="blog[connection.drop_database.something]"
By the time the recursive send fails on "something=", the database has
already been wiped. Well, this example doesn't really work (drop_database
requires an argument), but you get the idea.

Daniel
 
C

Curtis Summers

But I then realized that was a major security hole. It allows an attacker
to post stuff like:
<input name="blog[connection.drop_database.something]"
By the time the recursive send fails on "something=", the database has
already been wiped. Well, this example doesn't really work (drop_database
requires an argument), but you get the idea.

Well, if you are going to send an unescaped, form submitted value to
rsend, then, yes, that would be a security hole. But that's kind of
like saying you're going to allow an unescaped, client submitted value
to eval--which would be silly.

My usage of this is more along the lines of:

RoR controller w/ several actions that will render the same view. The
date that I want to show in that view might be one of several choices
of variable method depth depending on the action being rendered. So,
in each action I set the appropriate method call chain to pass to
rsend, and then use that variable in the view.

Here's a contrived example:

#controller
def action1
@posts.find:)all, :include => :comments)
@use_this_date = [:posted_at]
render :template => 'posts/list'
end

def action2
@posts.find:)all, :include => :comments)
@use_this_date = [:comments, :first, :commented_at]
render :template => 'posts/list'
end

#view
<% @posts.each do |post| %>
<%= h post.title %>,
<%= h post.rsend(*@use_this_date) %>
<% end %>
 
D

Daniel DeLorme

Curtis said:
But I then realized that was a major security hole. It allows an attacker
to post stuff like:
<input name="blog[connection.drop_database.something]"
By the time the recursive send fails on "something=", the database has
already been wiped. Well, this example doesn't really work (drop_database
requires an argument), but you get the idea.

Well, if you are going to send an unescaped, form submitted value to
rsend, then, yes, that would be a security hole. But that's kind of
like saying you're going to allow an unescaped, client submitted value
to eval--which would be silly.

Heheh. True to a certain extent, but while sending an unescaped string to
eval is obviously crazy, send seems safer. After all, RoR relies on stuff
like <input name="obj[field]" absolutely all over the place. So it may not
be immediately apparent that <input name="obj[field.subfield]" is far more
dangerous.

But on the original topic, my own implementation of rsend was more like this:
class Object
def rsend(msg, *others)
result = send(msg)
result = result.rsend(*others) unless others.empty?
result
end
end
This allows a class to override the rsend method in order to provide specific
behavior, e.g. in case send(msg) returns nil.

Daniel
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,968
Messages
2,570,150
Members
46,696
Latest member
BarbraOLog

Latest Threads

Top