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 '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 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
It's 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 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'] = '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
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.
Related
I want to test a following method, which calls a module method with a block.
def test_target
MyModule.send do |payload|
payload.my_text = "payload text"
end
end
MyModule's structure is like following.
module MyModule
class Payload
attr_accessor :my_text
def send
# do things with my_text
end
end
class << self
def send
payload = Payload.new
yield payload
payload.send
end
end
How can I test whether MyModule receives send method with a block, which assigns "payload text" to payload.my_text?
Currently I'm only testing expect(MyModule).to receive(:send).once. I looked through and tried Rspec yield matchers but cannot get things done. (Maybe I've ben searching for wrong keywords..)
The easiest way is to insert a double as the yield argument, which you can make an assertion on.
payload = Payload.new
allow(Payload).to receive(:new).and_return(payload)
test_target
expect(payload.my_text).to eq 'payload text'
Alternatively you could also use expect_any_instance_of, but I'd always prefer to use a specific double instead.
I would mock MyModule to yield another mock, that would allow speccing that my_text= is called on the yielded object.
let(:payload) { instance_double('Payload') }
before do
allow(MyModule).to receive(:send).and_yield(payload)
allow(payload).to receive(:my_text=).and_return(nil)
end
# expectations
expect(MyModule).to have_received(:send).once
expect(payload).to have_received(:my_text=).with('payload text').once
Below is a simple rspec example:
describe 'Emails' do
email_ids.each do |email_id|
it "should display #{email_id}" do
end
end
end
def email_ids
[
'test1#example.com',
'test2#example.com',
'test3#example.com'
]
end
The above does not work, as methods are not accessible outside the it block.
Please advise how to make the method email_ids accessible outside the it block.
describe creates a (nested) class and evaluates the given block within that class:
describe 'Foo' do
p self #=> RSpec::ExampleGroups::Foo
describe '' do
p self #=> RSpec::ExampleGroups::Foo::Bar
end
end
it blocks on the other hand are evaluated in the corresponding class' instance:
describe 'Foo' do
it 'foo' do
p self #=> #<RSpec::ExampleGroups::Foo ...>
end
end
If you define a method via def email_ids, it becomes an instance method and is therefore only available within the instance, i.e. within it.
In order to make a method available within describe, you have to define it as a class method, i.e via def self.email_ids:
describe 'Emails' do
def self.email_ids
%w[test1#example.com test2#example.com test3#example.com]
end
email_ids.each do |email_id|
it "should display #{email_id}" do
end
end
end
Output:
Emails
should display test1#example.com
should display test2#example.com
should display test3#example.com
You can also reuse the helper method across multiple tests by putting it in a module and using extend. See Define helper methods in a module for more examples.
I have better solution for this, than above.
1.using 'procs' or just local variable as below:
email_ids = ->{ %w[test1#example.com test2#example.com test3#example.com] }
email_ids = { %w[test1#example.com test2#example.com test3#example.com] }
Scope of proc & local variable will be same, but if you want to pass an argument then 'procs' are useful.
2.Define 'email_ids' method in module and include that module in spec, so that method will be accessible inside and outside the 'it' block
module EmailFactoryHelper
def email_ids
%w[test1#example.com test2#example.com test3#example.com]
end
end
include in specs as below:
require 'factories_helper/email_factory_helper'
include EmailFactoryHelper
describe 'Emails' do
email_ids.call.each do |email_id|
it "should display #{email_id}" do
page.should have_content "#{email_id}"
end
end
end
Output:
Emails
should display test1#example.com
should display test2#example.com
should display test3#example.com
Finished in 41.56 seconds
3 examples, 0 failures
I have preferred step-2
Rather than using proc or scopes,
Simply use local variables outside describe block.
email_ids = [
'test1#example.com',
'test2#example.com',
'test3#example.com'
]
describe 'Emails' do
end
The solution is to simply define your structure within scope, instead of returning it from a method call:
EMAILS = [
'test1#example.com',
'test2#example.com',
'test3#example.com'
]
EMAILS.each do |email|
it "should display #{email}" do
end
end
The method wasn't accessible because you called the method before you defined the method. This simpler script has the same problem:
p email_ids
def email_ids
[
'test1#example.com',
'test2#example.com',
'test3#example.com'
]
end
"undefined local variable or method `email_ids' for main:Object (NameError)"
You must define your methods before you call them. You can solve this problem by moving the def email_ids above the describe 'Emails'.
short version of #stefan's answer:
needs to be
def self.email_ids
# stuff
end
(def self.method for context/describe/etc; def method for it/etc)
I am wondering if there is an easy way to pass all local variables when calling a method, instead of passing them one by one as parameters. I want the following:
class Greeting
def hello
message = 'HELLO THERE!'
language = 'English'
say_greeting
end
def konnichiwa
message = 'KONNICHIWA!'
language = 'Japanese'
say_greeting
end
private def say_greeting
puts message
end
end
Greeting.new.hello
to show HELLO THERE!. But it returns an error: NameError: undefined local variable or method 'message'.
I tried local_variables to get all local variables as an Array of symbols. But I can't seem to access the actual variables because there seemed to be no Ruby method like local_variable_get.
Background of the problem
In my Rails application, I have a controller having three update methods for different view files. All of them behave exactly the same (that they all will update a resource, show error if any, etc). Their only main difference are
template to be rendered when unsuccessful
redirect url when successful
flash success message
I only have three variables, so it really is indeed easy to just pass them as parameters, but I am just wondering if there is an elegant solution to this.
Local variables are there to do precisely not what you are trying to do: encapsulate reference within a method definition. And instance variables are there to do precisely what you are trying to: sharing information between different method calls on a single object. Your use of local variable goes against that.
Use instance variables, not local variables.
If you insist on referencing local variables between methods, then here is a way:
class Greeting
def hello
message = 'HELLO THERE!'
language = 'English'
say_greeting(binding)
end
def konnichiwa
message = 'KONNICHIWA!'
language = 'Japanese'
say_greeting(binding)
end
private def say_greeting b
puts b.local_variable_get(:message)
end
end
You can't make it without passing any arguments, but you can just pass a single binding argument, and refer all local variables from there.
Make the message and language as instance variables.
Then you can access them inside the private method.
class Greeting
def hello
#message = 'HELLO THERE!'
#language = 'English'
say_greeting
end
def konnichiwa
#message = 'KONNICHIWA!'
#language = 'Japanese'
say_greeting
end
private
def say_greeting
puts #message
end
end
puts Greeting.new.hello
Use instance variables. In your method say_greeting make sure you call the method hello first, otherwise the value of #message will be nil.
def hello
#message = "Hello there"
end
def say_greeting
hello #method call
puts message
end
I'm a newb working through some Ruby tutorials and am stumped on the use of the send method below. I can see the send method is reading the value of the attribute iterator over, but the Ruby documentation states the send method takes a method prepended with a colon. So, my confusion lies in how the send method below is interpolating the attribute variable being iterated over.
module FormatAttributes
def formats(*attributes)
#format_attribute = attributes
end
def format_attributes
#format_attributes
end
end
module Formatter
def display
self.class.format_attributes.each do |attribute|
puts "[#{attribute.to_s.upcase}] #{send(attribute)}"
end
end
end
class Resume
extend FormatAttributes
include Formatter
attr_accessor :name, :phone_number, :email, :experience
formats :name, :phone_number, :email, :experience
end
It's not "invoking the value of the iterator", but instead calling a method with that name. In this case because of the attr_accessor declaration, these methods map to properties.
Calling object.send('method_name') or object.send(:method_name) are equivalent to object.method_name in general terms. Likewise, send(:foo) and foo will call the method foo on the context.
Since the module declare a method that is later mixed in with an include, calling send in the module has the effect of calling a method on an instance of the Resume class.
send Documentation
here is simplified version of your code,to feed you what's going on:
def show
p "hi"
end
x = "show"
y = :show
"#{send(y)}" #=> "hi"
"#{send(x)}" #=> "hi"
Essentially I'm wondering how to place callbacks on objects in ruby, so that when an object is changed in anyway I can automatically trigger other changes:
(EDIT: I confused myself in my own example! Not a good sign… As #proxy is a URI object it has it's own methods, changing the URI object by using it's own methods doesn't call my own proxy= method and update the #http object)
class MyClass
attr_reader :proxy
def proxy=(string_proxy = "")
begin
#proxy = URI.parse("http://"+((string_proxy.empty?) ? ENV['HTTP_PROXY'] : string_proxy))
#http = Net::HTTP::Proxy.new(#proxy.host,#proxy.port)
rescue
#http = Net::HTTP
end
end
end
m = MyClass.new
m.proxy = "myproxy.com:8080"
p m.proxy
# => <URI: #host="myproxy.com" #port=8080>
m.proxy.host = 'otherproxy.com'
p m.proxy
# => <URI: #host="otherproxy.com" #port=8080>
# But accessing a website with #http.get('http://google.com') will still travel through myproxy.com as the #http object hasn't been changed when m.proxy.host was.
Your line m.proxy = nil will raise a NoMethodError exception, since nil does no respond to empty?. Thus #http is set to Net::HTTP, as in the rescue clause.
This has nothing to do with callbacks/setters. You should modify your code to do what you want (e.g. calling string_proxy.blank? if using activesupport).
I managed to figure this one out for myself!
# Unobtrusive modifications to the Class class.
class Class
# Pass a block to attr_reader and the block will be evaluated in the context of the class instance before
# the instance variable is returned.
def attr_reader(*params,&block)
if block_given?
params.each do |sym|
# Create the reader method
define_method(sym) do
# Force the block to execute before we…
self.instance_eval(&block)
# … return the instance variable
self.instance_variable_get("##{sym}")
end
end
else # Keep the original function of attr_reader
params.each do |sym|
attr sym
end
end
end
end
If you add that code somewhere it'll extend the attr_reader method so that if you now do the following:
attr_reader :special_attr { p "This happens before I give you #special_attr" }
It'll trigger the block before it gives you the #special_attr. It's executed in the instance scope so you can use it, for example, in classes where attributes are downloaded from the internet. If you define a method like get_details which does all retrieval and sets #details_retrieved to true then you can define the attr like this:
attr_reader :name, :address, :blah { get_details if #details_retrieved.nil? }