Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the order of using blocks in Ruby

Tags:

ruby

block

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
like image 723
DenicioCode Avatar asked Jul 27 '15 10:07

DenicioCode


People also ask

Why do we use begin and end in Ruby?

Ruby's BEGIN and END blocks (in uppercase) are reserved keywords in Ruby and are pretty straightforward to use. They enable you to specify blocks of code that you want your program to run at the beginning and end of its execution, regardless of their position in the source file.

What are Ruby blocks explain with example?

Ruby blocks are anonymous functions that can be passed into methods. Blocks are enclosed in a do-end statement or curly braces {}. do-end is usually used for blocks that span through multiple lines while {} is used for single line blocks. Blocks can have arguments which should be defined between two pipe | characters.

How do you write blocks in Ruby?

Ruby blocks are little anonymous functions that can be passed into methods. Blocks are enclosed in a do / end statement or between brackets {} , and they can have multiple arguments.


1 Answers

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
like image 168
Sergio Tulentsev Avatar answered Oct 19 '22 02:10

Sergio Tulentsev