DenicioCode
DenicioCode

Reputation: 9346

What is the order of using blocks in Ruby

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. enter image description here

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

Answers (2)

ndnenkov
ndnenkov

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

Sergio Tulentsev
Sergio Tulentsev

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)

Alternative approach

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'

Code

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

Related Questions