Using method missing to create getters and setters

T

Tim Conner

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:=> ""
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:=> 45

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

Thanks a lot.
 
R

Robert Dober

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
 
R

Robert Klemme

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}"

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.
self.send("message_thread_#{$1}=", $2)
else
super
end
end
end

However, this is not working. See below:
=> ""
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:
=> 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
 
T

Tim Hunter

Tim said:
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:=> ""
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:=> 45

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

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').
 
T

Tim Conner

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?
 
R

Rick DeNatale

On 26.04.2009 19:40, Tim Conner wrote:
My code currently looks like this:
class BatchExternalBooking
=A0def method_missing(method_sym, *args)
=A0 =A0if method_sym.to_s =3D~ /^message_thread_([0-9]*)=3D?(\w*)?$/
=A0 =A0 =A0BatchExternalBooking.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 metho= d
you invoke.

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}=3D", *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.

--=20
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
 
R

Robert Klemme

On 26.04.2009 19:40, Tim Conner 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.

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.
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
 
B

Brian Candler

Tim said:
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.
 
D

David Masover

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.
 
R

Robert Dober

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 che=
ck
Well apart that Robert has said this already nothing to be sorry about. ;)
This is a very concise implementation and I prefer it to mine.
Cheers
Robert
 

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,769
Messages
2,569,576
Members
45,054
Latest member
LucyCarper

Latest Threads

Top