What is the use order of blocks in Ruby

I am creating a gem to support some command line mailings. I use some gem. I am using Mail Gem . As you can see in the mail gem description, this is something like this.

 mail = Mail.new do from ' mikel@test.lindsaar.net ' to ' you@test.lindsaar.net ' subject 'This is a test email' body File.read('body.txt') end 

In the block, I call methods from the Mail class (from, to, subject, body). That makes sense, so I create it in my mail program 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 

It looks straight. Just call the block and fill in my values ​​that I get through the constructor. Now my question comes.

I tried to put the body structure for mail in a separate method. But I cannot use it in the Mail constructor for a 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 exited this code. enter image description here

This means that I cannot use my class method or class variable (starting with @a ) in this block.

Questions

What is the execution order in the block? If I set my @mail_settings variable, I cannot use it in a block. Is Ruby to search @mail_settings in the Mail class where I give the block? Why can I use this parameter from the BossMailer::Mailer constructor through the block and an error does not appear?

And why does this work if I use a variable to parse the contents in 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 
+5
source share
2 answers

All about the context.

 mail = Mail.new do from ' mikel@test.lindsaar.net ' to ' you@test.lindsaar.net ' subject 'This is a test email' body File.read('body.txt') end 

from , to methods (and the rest) are methods in the Mail::Message instance. So that you can call them in this nice DSL manner, the block that you pass to the constructor is instance_eval'ed .

This means that inside this block, self no longer your mail program, but instead a mail message. As a result, your email method is not available.

Instead of instance_eval they can only have yield or block.call , but this will not make DSL possible.

What is the reason why a local variable works: since ruby ​​blocks are closed lexically closed (this means that they retain the local context of their declaration. If there was a local variable visible from where the block is defined, then it will remember the variable and its value when the block is called)

Alternative approach

Do not use a block shape. Use this: https://github.com/mikel/mail/blob/0f9393bb3ef1344aa76d6dac28db3a4934c65087/lib/mail/message.rb#L92-L96

 mail = Mail.new mail['from'] = ' mikel@test.lindsaar.net ' mail[:to] = ' you@test.lindsaar.net ' 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 = ' vasya@example.com ' Mail.new do to get_recipient end end def get_recipient ' sergio@example.com ' end end Mailer.new.admin_mail 
+3
source

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 

It will help you

 hi there, a hi there, b `block in test': undefined method `hi' for #<C:0x286e1c8> (NoMethodError) 

Looking at the gem code, this is exactly what happens:

Mail.new simply passes the block specified by the Mail::Message constructor .

The specified constructor works exactly the same as in the case of C above .


instance_eval basically modifies what self is in the current context.

About why the cases of B and C work differently - you may think that & will "change" the block object from proc to block (yes, my choice of the variable name was small there), More about the difference here .

+2
source

All Articles