Using method missing to create getters and setters


#1

Hi,
I am trying to create a model (BatchExternalBooking) which has the
following methods:
BatchExternalBooking.message_thread_1=
BatchExternalBooking.message_thread_1
BatchExternalBooking.message_thread_2=
BatchExternalBooking.message_thread_2
… etc all the way upto
BatchExternalBooking.message_thread_x=
BatchExternalBooking.message_thread_x

I assume that I need to do this via method_missing because I don’t know
how many of these methods I will actually need.

My code currently looks like this:
class BatchExternalBooking
def method_missing(method_sym, args)
if method_sym.to_s =~ /^message_thread_([0-9]
)=?(\w*)?$/
BatchExternalBooking.instance_eval “attr_accessor
:message_thread_#{$1}”
self.send(“message_thread_#{$1}=”, $2)
else
super
end
end
end

However, this is not working. See below:

b = BatchExternalBooking.new
=> #BatchExternalBooking:0x3e54040

b.message_thread_1 = 45
=> 45

b.message_thread_1
=> “”
The value wasn’t actually set but the attr_accessor correctly created
the setter method. If I try and set the variable again, it works:

b.message_thread_1 = 45
=> 45

b.message_thread_1
=> 45

Why isn’t the setter working the first time round?

Thanks a lot.


#2

Hmm seems a little confusing with the $1 in the evals and $2 is
outright wrong you mean args.first

def method_missing sym, *args
name = sym.to_s
aname = name.sub("=","")

super unless aname =~ /whatever/

self.class.module_eval do # just a matter of taste
attr_accessor aname
end
send name, args.first unless aname == name
end

HTH
Robert


#3

Tim C. wrote:

else

b.message_thread_1
Thanks a lot.
Two thoughts. First, why not use an array? Then

BatchExternalBooking.message_thread[n] = whatever

If that doesn’t work for you, then investigate OpenStruct (i.e.
‘ostruct’).


#4

On 26.04.2009 19:40, Tim C. wrote:

I assume that I need to do this via method_missing because I don’t know
how many of these methods I will actually need.

My code currently looks like this:
class BatchExternalBooking
def method_missing(method_sym, args)
if method_sym.to_s =~ /^message_thread_([0-9]
)=?(\w*)?$/
BatchExternalBooking.instance_eval “attr_accessor
:message_thread_#{$1}”

You might rather want to use class_eval here.

Also, as Robert pointed out already, using $1 only once and storing the
value in a local variable is safer because it can be changed by any
method you invoke.

b.message_thread_1 = 45
Why isn’t the setter working the first time round?

Thanks a lot.

Is there a reason that you do not use OpenStruct or an ordinary Array
for this? It seems you are indexing by number anyway so why not use an
Array?

Kind regards

robert


#5

The reason that I am trying to use methods/attributes to store the
information rather than an array is because this model is used to create
a batch update form in a Rails project. If one of the updates fails
then I want to display the errors on the form next to the relevant
fields. For this to work, I will need to use the error_messages_for
helper to which I need to pass the attribute.

Am I approaching this in the correct manner?


#6

On Sun, Apr 26, 2009 at 3:30 PM, Robert K.
removed_email_address@domain.invalid wrote:

On 26.04.2009 19:40, Tim C. wrote:

My code currently looks like this:
class BatchExternalBooking
def method_missing(method_sym, args)
if method_sym.to_s =~ /^message_thread_([0-9]
)=?(\w*)?$/
BatchExternalBooking.instance_eval “attr_accessor
:message_thread_#{$1}”

You might rather want to use class_eval here.

No instance_eval works just as well for sending attr_accessor to a
class/module. The only difference with class_eval is when using def to
define a method.

Both do the evaluation in the context of the receive but class_eval
sets the current class to the receiver.

Class.class_eval(“def foo;end”) defines an instance method while
Class.instance_eval(“def foo;end”) defines a class method.

This seems a bit less odd when you consider that a class method is
just a singleton method on the class (albeit one which can be
inherited by subclasses).

Also, as Robert pointed out already, using $1 only once and storing the
value in a local variable is safer because it can be changed by any method
you invoke.

 self.send("message_thread_#{$1}=", $2)

That’s not true either. The $n variables are frame local, in a given
invocation frame they will only change when another regex match is
done in the same invocation. The bug here is that this line should
have been

   self.send("message_thread_#{$1}=", *args)  # or args.first if you 

must.

Since the regexp only had a single capture, $2 is always nil, so even
though the newly created setter is geting called, it’s setting the
instance variable to nil.


Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale
WWR: http://www.workingwithrails.com/person/9021-rick-denatale
LinkedIn: http://www.linkedin.com/in/rickdenatale


#7

On 26.04.2009 21:55, Rick DeNatale wrote:

You might rather want to use class_eval here.

No instance_eval works just as well for sending attr_accessor to a
class/module. The only difference with class_eval is when using def to
define a method.

I did not want to state that instance_eval is the issue. Sorry for
being imprecise. A simple

BatchExternalBooking.send “attr_accessor”, “message_thread_#$1”

would be sufficient.

Also, as Robert pointed out already, using $1 only once and storing the
value in a local variable is safer because it can be changed by any method
you invoke.

 self.send("message_thread_#{$1}=", $2)

That’s not true either. The $n variables are frame local, in a given
invocation frame they will only change when another regex match is
done in the same invocation.

A simple test verifies this to be true. But now I wonder how I have
come to this misconception. I am pretty sure I stored $1 in a local
variable to avoid issues with changing values. Maybe I just had another
match in the same method and extended the overwriting problem to method
calls.

Thank you for the education, Rick!

Kind regards

robert


#8

Tim C. wrote:

The reason that I am trying to use methods/attributes to store the
information rather than an array is because this model is used to create
a batch update form in a Rails project. If one of the updates fails
then I want to display the errors on the form next to the relevant
fields. For this to work, I will need to use the error_messages_for
helper to which I need to pass the attribute.

Am I approaching this in the correct manner?

I believe that ostruct by itself will do the job, or you can use
method_missing to delegate to a hash without actually defining any
methods.

At least, I know this works for form helpers. You’d have to test it with
validations and error_messages_for.


#9

On Mon, Apr 27, 2009 at 11:44 PM, David M. removed_email_address@domain.invalid
wrote:

On Sunday 26 April 2009 12:40:05 Tim C. wrote:

  BatchExternalBooking.instance_eval "attr_accessor

Why the string eval?

BatchExternalBooking.send :attr_accessor, :“message_thread_#{$1}”

Sorry, I’m a pedant about things like that… Also, you might want to check
Well apart that Robert has said this already nothing to be sorry about.
:wink:
This is a very concise implementation and I prefer it to mine.
Cheers
Robert


#10

On Sunday 26 April 2009 12:40:05 Tim C. wrote:

  BatchExternalBooking.instance_eval "attr_accessor

Why the string eval?

BatchExternalBooking.send :attr_accessor, :“message_thread_#{$1}”

Sorry, I’m a pedant about things like that… Also, you might want to
check
that this does the right thing when the getter is called before the
setter. At
least, your method_missing regex seems to assume that this might be the
case,
but your code doesn’t.