Reputation: 9346
I am creating a gem to support some Mailing from the command line. I use some Gem.
I am using the Mail Gem. As you can see in the description of mail gem
is something like this.
mail = Mail.new do
from '[email protected]'
to '[email protected]'
subject 'This is a test email'
body File.read('body.txt')
end
In the block I call the methods from the Mail
class (from, to, subject, body). This makes sense so I build it in my own mailer class
def initialize(mail_settings, working_hours)
@mail_settings = mail_settings
@working_hours = working_hours
@mailer = Mail.new do
to mail_settings[:to]
from mail_settings[:from]
subject mail_settings[:subject]
body "Start #{working_hours[:start]} \n\
Ende #{working_hours[:end]}\n\
Pause #{working_hours[:pause]}"
end
end
This looks straight forward. Just call the block und fill in my values I get through the constructor. Now comes my question.
I tried to put out the body construction for the mail into a separated method. But I cannot use it in the Mail
constructor of the gem.
module BossMailer
class Mailer
def initialize(mail_settings, working_hours)
@mail_settings = mail_settings
@working_hours = working_hours
@mailer = Mail.new do
to mail_settings[:to]
from mail_settings[:from]
subject mail_settings[:subject]
body mail_body
end
end
def mail
@mailer.delivery_method :smtp, address: "localhost", port: 1025
@mailer.deliver
end
def mail_body
"Start #{working_hours[:start]} \n\
Ende #{working_hours[:end]}\n\
Pause #{working_hours[:pause]}"
end
end
end
This error came out this code.
That means I cannot use my class method or class variable (beginning with @a
) in this block.
Questions
What is the order of the execution in a Block? If I set my variable @mail_settings
, I can't use it in the block. Is Ruby searching for @mail_settings
in Mail
class where I give the block to? Why can I use the given parameter from the BossMailer::Mailer
constructor through the block and no error appears?
And why does this works if I am using and variable to parse the content into the block? (body_content = mail_body
) works!
def initialize(mail_settings, working_hours)
@mail_settings = mail_settings
@working_hours = working_hours
body_content = mail_body
@mailer = Mail.new do
to mail_settings[:to]
from mail_settings[:from]
subject mail_settings[:subject]
body body_content
end
end
Upvotes: 5
Views: 170
Reputation: 36110
The problem is that mail_body
is evaluated in the context of Mail::Message
and not in the context of your BossMailer::Mailer
class. Consider the following examples:
class A
def initialize
yield
end
end
class B
def initialize(&block)
instance_eval { block.call }
end
end
class C
def initialize(&block)
instance_eval(&block)
end
end
class Caller
def test
A.new { hi 'a' }
B.new { hi 'b' }
C.new { hi 'c' }
end
def hi(x)
puts "hi there, #{x}"
end
end
Caller.new.test
This will get you
hi there, a
hi there, b
`block in test': undefined method `hi' for #<C:0x286e1c8> (NoMethodError)
Looking at the gem's code, this is exactly what happens:
Mail.new
just passes the block given to Mail::Message
's constructor.
The said constructor works exactly as the C
case above.
instance_eval
basically changes what self
is in the current context.
About why B
and C
cases work differently - you can think that &
will 'change' the block
object from proc to block (yes, my choice of variable name wasn't great there). More on the difference here.
Upvotes: 2
Reputation: 230531
It's all about the context.
mail = Mail.new do
from '[email protected]'
to '[email protected]'
subject 'This is a test email'
body File.read('body.txt')
end
from
, to
methods (and the rest) are methods on Mail::Message
instance. For you to be able to call them in this nice DSL-manner, the block you pass to constructor is instance_eval'ed.
What this means is that inside of this block, self
is no longer your mailer, but a mail message instead. As a result, your mailer method is not accessible.
Instead of instance_eval
, they could have just yield
or block.call
, but this wouldn't make the DSL possible.
As to why the local variable works: it's because ruby blocks are lexically-scoped closures (meaning, they retain local context of their declaration. If there was a local variable visible from where the block is defined, it'll remember the variable and its value when the block is called)
Don't use the block form. Use this: https://github.com/mikel/mail/blob/0f9393bb3ef1344aa76d6dac28db3a4934c65087/lib/mail/message.rb#L92-L96
mail = Mail.new
mail['from'] = '[email protected]'
mail[:to] = '[email protected]'
mail.subject 'This is a test email'
mail.body = 'This is a body'
Try commenting/uncommenting some lines.
class Mail
def initialize(&block)
# block.call(self) # breaks DSL
instance_eval(&block) # disconnects methods of mailer
end
def to(email)
puts "sending to #{email}"
end
end
class Mailer
def admin_mail
# get_recipient = '[email protected]'
Mail.new do
to get_recipient
end
end
def get_recipient
'[email protected]'
end
end
Mailer.new.admin_mail
Upvotes: 3