Skip to content

Commit

Permalink
Offer the option to use parameterization for shared processing of hea…
Browse files Browse the repository at this point in the history
…ders and ivars (#27825)

Offer the option to use parameterization for shared processing of headers and ivars
  • Loading branch information
dhh authored Jan 28, 2017
1 parent 05112b2 commit 1cec84a
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 7 deletions.
9 changes: 9 additions & 0 deletions actionmailer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
* Add parameterized invocation of mailers as a way to share before filters and defaults between actions.
See ActionMailer::Parameterized for a full example of the benefit.

*DHH*

* Allow lambdas to be used as lazy defaults in addition to procs.

*DHH*

* Mime type: allow to custom content type when setting body in headers
and attachments.

Expand Down
1 change: 1 addition & 0 deletions actionmailer/lib/action_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module ActionMailer
autoload :DeliveryMethods
autoload :InlinePreviewInterceptor
autoload :MailHelper
autoload :Parameterized
autoload :Preview
autoload :Previews, "action_mailer/preview"
autoload :TestCase
Expand Down
13 changes: 6 additions & 7 deletions actionmailer/lib/action_mailer/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,20 +288,19 @@ module ActionMailer
# content_description: 'This is a description'
# end
#
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
# can define methods that evaluate as the message is being generated:
# Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash,
# so you can define methods that evaluate as the message is being generated:
#
# class NotifierMailer < ApplicationMailer
# default 'X-Special-Header' => Proc.new { my_method }
# default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address }
#
# private
#
# def my_method
# 'some complex call'
# end
# end
#
# Note that the proc is evaluated right at the start of the mail message generation, so if you
# Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you
# set something in the default hash using a proc, and then set the same thing inside of your
# mailer method, it will get overwritten by the mailer method.
#
Expand All @@ -324,7 +323,6 @@ module ActionMailer
# end
#
# private
#
# def add_inline_attachment!
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
# end
Expand Down Expand Up @@ -434,6 +432,7 @@ module ActionMailer
class Base < AbstractController::Base
include DeliveryMethods
include Rescuable
include Parameterized
include Previews

abstract!
Expand Down Expand Up @@ -888,7 +887,7 @@ def apply_defaults(headers)
default_values = self.class.default.map do |key, value|
[
key,
value.is_a?(Proc) ? instance_eval(&value) : value
value.is_a?(Proc) ? instance_exec(&value) : value
]
end.to_h

Expand Down
141 changes: 141 additions & 0 deletions actionmailer/lib/action_mailer/parameterized.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
module ActionMailer
# Provides the option to parameterize mailers in other to share ivar setup, processing, and common headers.
#
# Consider this example that does not use parameterization:
#
# class InvitationsMailer < ApplicationMailer
# def account_invitation(inviter, invitee)
# @account = inviter.account
# @inviter = inviter
# @invitee = invitee
#
# subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
#
# def project_invitation(project, inviter, invitee)
# @account = inviter.account
# @project = project
# @inviter = inviter
# @invitee = invitee
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
#
# subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
#
# def bulk_project_invitation(projects, inviter, invitee)
# @account = inviter.account
# @projects = projects.sort_by(&:name)
# @inviter = inviter
# @invitee = invitee
#
# subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
#
# mail \
# subject: subject,
# to: invitee.email_address,
# from: common_address(inviter),
# reply_to: inviter.email_address_with_name
# end
# end
#
# InvitationsMailer.account_invitation(person_a, person_b).deliver_later
#
# Using parameterized mailers, this can be rewritten as:
#
# class InvitationsMailer < ApplicationMailer
# before_action { @inviter, @invitee = params[:inviter], params[:invitee] }
# before_action { @account = params[:inviter].account }
#
# default to: -> { @invitee.email_address },
# from: -> { common_address(@inviter) },
# reply_to: -> { @inviter.email_address_with_name }
#
# def account_invitation
# mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})"
# end
#
# def project_invitation
# @project = params[:project]
# @summarizer = ProjectInvitationSummarizer.new(@project.bucket)
#
# mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})"
# end
#
# def bulk_project_invitation
# @projects = params[:projects].sort_by(&:name)
#
# mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})"
# end
# end
#
# InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later
#
# That's a big improvement! It's also fully backwards compatible. So you can start to gradually transition
# mailers that stand to benefit the most from parameterization one by one and leave the others behind.
module Parameterized
extend ActiveSupport::Concern

included do
attr_accessor :params
end

class_methods do
def with(params)
ActionMailer::Parameterized::Mailer.new(self, params)
end
end

class Mailer
def initialize(mailer, params)
@mailer, @params = mailer, params
end

def method_missing(method_name, *args)
if @mailer.action_methods.include?(method_name.to_s)
ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, *args).tap { |pmd| pmd.params = @params }
else
super
end
end
end

class MessageDelivery < ActionMailer::MessageDelivery
attr_accessor :params

private
def processed_mailer
@processed_mailer ||= @mailer_class.new.tap do |mailer|
mailer.params = params
mailer.process @action, *@args
end
end

def enqueue_delivery(delivery_method, options = {})
if processed?
super
else
args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args
ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args)
end
end
end

class DeliveryJob < ActionMailer::DeliveryJob # :nodoc:
def perform(mailer, mail_method, delivery_method, params, *args) #:nodoc:
mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method)
end
end
end
end
11 changes: 11 additions & 0 deletions actionmailer/test/mailers/params_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class ParamsMailer < ActionMailer::Base
before_action { @inviter, @invitee = params[:inviter], params[:invitee] }

default to: Proc.new { @invitee }, from: -> { @inviter }

def invitation
mail(subject: "Welcome to the project!") do |format|
format.text { render plain: "So says #{@inviter}" }
end
end
end
44 changes: 44 additions & 0 deletions actionmailer/test/parameterized_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require "abstract_unit"
require "active_job"
require "mailers/params_mailer"

class ParameterizedTest < ActiveSupport::TestCase
include ActiveJob::TestHelper

setup do
@previous_logger = ActiveJob::Base.logger
ActiveJob::Base.logger = Logger.new(nil)

@original_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = :test

@previous_delivery_method = ActionMailer::Base.delivery_method
@previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name
ActionMailer::Base.deliver_later_queue_name = :test_queue
ActionMailer::Base.delivery_method = :test

@mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
end

teardown do
ActiveJob::Base.logger = @previous_logger
ParamsMailer.deliveries.clear

ActionMailer::Base.delivery_method = @original_delivery_method

ActionMailer::Base.delivery_method = @previous_delivery_method
ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name
end

test "parameterized headers" do
assert_equal(["jason@basecamp.com"], @mail.to)
assert_equal(["david@basecamp.com"], @mail.from)
assert_equal("So says david@basecamp.com", @mail.body.encoded)
end

test "should enqueue the email with params" do
assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do
@mail.deliver_later
end
end
end

0 comments on commit 1cec84a

Please sign in to comment.