Remove global state. Immutable mailer. (#69)

This commit is contained in:
Luca Guidi 2017-11-29 12:15:12 +01:00 committed by GitHub
parent dded5d2268
commit d2b8a32ade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1725 additions and 1500 deletions

View File

@ -2,3 +2,7 @@
# alphabetically # alphabetically
inherit_from: inherit_from:
- https://raw.githubusercontent.com/hanami/devtools/master/.rubocop.yml - https://raw.githubusercontent.com/hanami/devtools/master/.rubocop.yml
Style/Documentation:
Exclude:
- "examples/*"
- "spec/**/*"

View File

@ -2,8 +2,9 @@ source 'https://rubygems.org'
gemspec gemspec
unless ENV['TRAVIS'] unless ENV['TRAVIS']
gem 'byebug', require: false, platforms: :mri gem 'byebug', require: false, platforms: :mri
gem 'yard', require: false gem 'allocation_stats', require: false
gem 'benchmark-ips', require: false
end end
gem 'hanami-utils', '2.0.0.alpha1', require: false, git: 'https://github.com/hanami/utils.git', branch: 'unstable' gem 'hanami-utils', '2.0.0.alpha1', require: false, git: 'https://github.com/hanami/utils.git', branch: 'unstable'

155
README.md
View File

@ -44,10 +44,10 @@ Or install it yourself as:
### Conventions ### Conventions
* Templates are searched under `Hanami::Mailer.configuration.root`, set this value according to your app structure (eg. `"app/templates"`). * Templates are searched under `Hanami::Mailer::Configuration#root`, set this value according to your app structure (eg. `"app/templates"`).
* A mailer will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`). * A mailer will look for a template with a file name that is composed by its full class name (eg. `"articles/index"`).
* A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`). * A template must have two concatenated extensions: one for the format and one for the engine (eg. `".html.erb"`).
* The framework must be loaded before rendering the first time: `Hanami::Mailer.load!`. * The framework must be loaded before rendering the first time: `Hanami::Mailer.finalize(configuration)`.
### Mailers ### Mailers
@ -55,10 +55,60 @@ A simple mailer looks like this:
```ruby ```ruby
require 'hanami/mailer' require 'hanami/mailer'
require 'ostruct'
class InvoiceMailer # Create two files: `invoice.html.erb` and `invoice.txt.erb`
include Hanami::Mailer
configuration = Hanami::Mailer::Configuration.new do |config|
config.delivery_method = :test
end end
class Invoice < Hanami::Mailer
from "noreply@example.com"
to ->(locals) { locals.fetch(:user).email }
end
configuration = Hanami::Mailer.finalize(configuration)
invoice = OpenStruct.new(number: 23)
mailer = InvoiceMailer.new(configuration: configuration)
mail = mailer.deliver(invoice: invoice)
mail
# => #<Mail::Message:70303354246540, Multipart: true, Headers: <Date: Wed, 22 Mar 2017 11:48:57 +0100>, <From: noreply@example.com>, <To: user@example.com>, <Cc: >, <Bcc: >, <Message-ID: <58d25699e47f9_b4e13ff0c503e4f4632e6@escher.mail>>, <Subject: >, <Mime-Version: 1.0>, <Content-Type: multipart/alternative; boundary=--==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186>, <Content-Transfer-Encoding: 7bit>>
mail.to_s
# =>
# From: noreply@example.com
# To: user@example.com
# Message-ID: <58d25699e47f9_b4e13ff0c503e4f4632e6@escher.mail>
# Subject:
# Mime-Version: 1.0
# Content-Type: multipart/alternative;
# boundary="--==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186";
# charset=UTF-8
# Content-Transfer-Encoding: 7bit
#
#
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186
# Content-Type: text/plain;
# charset=UTF-8
# Content-Transfer-Encoding: 7bit
#
# Invoice #23
#
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186
# Content-Type: text/html;
# charset=UTF-8
# Content-Transfer-Encoding: 7bit
#
# <html>
# <body>
# <h1>Invoice template</h1>
# </body>
# </html>
#
# ----==_mimepart_58d25699e42d2_b4e13ff0c503e4f463186--
``` ```
A mailer with `.to` and `.from` addresses and mailer delivery: A mailer with `.to` and `.from` addresses and mailer delivery:
@ -66,20 +116,18 @@ A mailer with `.to` and `.from` addresses and mailer delivery:
```ruby ```ruby
require 'hanami/mailer' require 'hanami/mailer'
Hanami::Mailer.configure do configuration = Hanami::Mailer::Configuration.new do |config|
delivery_method :smtp, config.delivery_method = :smtp,
address: "smtp.gmail.com", address: "smtp.gmail.com",
port: 587, port: 587,
domain: "example.com", domain: "example.com",
user_name: ENV['SMTP_USERNAME'], user_name: ENV['SMTP_USERNAME'],
password: ENV['SMTP_PASSWORD'], password: ENV['SMTP_PASSWORD'],
authentication: "plain", authentication: "plain",
enable_starttls_auto: true enable_starttls_auto: true
end.load! end
class WelcomeMailer
include Hanami::Mailer
class WelcomeMailer < Hanami::Mailer
from 'noreply@sender.com' from 'noreply@sender.com'
to 'noreply@recipient.com' to 'noreply@recipient.com'
cc 'cc@sender.com' cc 'cc@sender.com'
@ -88,7 +136,7 @@ class WelcomeMailer
subject 'Welcome' subject 'Welcome'
end end
WelcomeMailer.deliver WelcomeMailer.new(configuration: configuration).call(locals)
``` ```
### Locals ### Locals
@ -97,25 +145,17 @@ The set of objects passed in the `deliver` call are called `locals` and are avai
```ruby ```ruby
require 'hanami/mailer' require 'hanami/mailer'
require 'ostruct'
User = Struct.new(:name, :username, :email) user = OpenStruct.new(name: Luca', email: 'user@hanamirb.org')
luca = User.new('Luca', 'jodosha', 'luca@jodosha.com')
class WelcomeMailer
include Hanami::Mailer
class WelcomeMailer < Hanami::Mailer
from 'noreply@sender.com' from 'noreply@sender.com'
subject 'Welcome' subject 'Welcome'
to :recipient to ->(locals) { locals.fetch(:user).email }
private
def recipient
user.email
end
end end
WelcomeMailer.deliver(user: luca) WelcomeMailer.new(configuration: configuration).deliver(user: luca)
``` ```
The corresponding `erb` file: The corresponding `erb` file:
@ -131,9 +171,7 @@ All public methods defined in the mailer are accessible from the template:
```ruby ```ruby
require 'hanami/mailer' require 'hanami/mailer'
class WelcomeMailer class WelcomeMailer < Hanami::Mailer
include Hanami::Mailer
from 'noreply@sender.com' from 'noreply@sender.com'
to 'noreply@recipient.com' to 'noreply@recipient.com'
subject 'Welcome' subject 'Welcome'
@ -154,7 +192,7 @@ The template file must be located under the relevant `root` and must match the i
```ruby ```ruby
# Given this root # Given this root
Hanami::Mailer.configuration.root # => #<Pathname:app/templates> configuration.root # => #<Pathname:app/templates>
# For InvoiceMailer, it looks for: # For InvoiceMailer, it looks for:
# * app/templates/invoice_mailer.html.erb # * app/templates/invoice_mailer.html.erb
@ -164,9 +202,7 @@ Hanami::Mailer.configuration.root # => #<Pathname:app/templates>
If we want to specify a different template, we can do: If we want to specify a different template, we can do:
```ruby ```ruby
class InvoiceMailer class InvoiceMailer < Hanami::Mailer
include Hanami::Mailer
template 'invoice' template 'invoice'
end end
@ -311,21 +347,22 @@ It supports a few options:
```ruby ```ruby
require 'hanami/mailer' require 'hanami/mailer'
Hanami::Maler.configure do configuration = Hanami::Mailer::Configuration.new do |config|
# Set the root path where to search for templates # Set the root path where to search for templates
# Argument: String, Pathname, #to_pathname, defaults to the current directory # Argument: String, Pathname, #to_pathname, defaults to the current directory
# #
root '/path/to/root' config.root = 'path/to/root'
# Set the default charset for emails # Set the default charset for emails
# Argument: String, defaults to "UTF-8" # Argument: String, defaults to "UTF-8"
# #
default_charset 'iso-8859' config.default_charset = 'iso-8859'
# Set the delivery method # Set the delivery method
# Argument: Symbol # Argument: Symbol
# Argument: Hash, optional configurations # Argument: Hash, optional configurations
delivery_method :stmp config.delivery_method = :stmp
end
``` ```
### Attachments ### Attachments
@ -333,14 +370,10 @@ Hanami::Maler.configure do
Attachments can be added with the following API: Attachments can be added with the following API:
```ruby ```ruby
class InvoiceMailer class InvoiceMailer < Hanami::Mailer
include Hanami::Mailer
# ... # ...
before do |mail, locals|
def prepare mail.attachments["invoice-#{locals.fetch(:invoice).number}.pdf"] = 'path/to/invoice.pdf'
mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf'
# or
mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
end end
end end
``` ```
@ -350,14 +383,14 @@ end
The global delivery method is defined through the __Hanami::Mailer__ configuration, as: The global delivery method is defined through the __Hanami::Mailer__ configuration, as:
```ruby ```ruby
Hanami::Mailer.configuration do configuration = Hanami::Mailer::Configuration.new do |config|
delivery_method :smtp config.delivery_method = :smtp
end end
``` ```
```ruby ```ruby
Hanami::Mailer.configuration do configuration = Hanami::Mailer::Configuration.new do |config|
delivery_method :smtp, address: "localhost", port: 1025 config.delivery_method = :smtp, { address: "localhost", port: 1025 }
end end
``` ```
@ -386,14 +419,14 @@ class MandrillDeliveryMethod
end end
end end
Hanami::Mailer.configure do configuration = Hanami::Mailer::Configuration.new do |config|
delivery_method MandrillDeliveryMethod, config.delivery_method = MandrillDeliveryMethod,
username: ENV['MANDRILL_USERNAME'], username: ENV['MANDRILL_USERNAME'],
password: ENV['MANDRILL_API_KEY'] password: ENV['MANDRILL_API_KEY']
end.load! end
``` ```
The class passed to `.delivery_method` must accept an optional set of options The class passed to `.delivery_method=` must accept an optional set of options
with the constructor (`#initialize`) and respond to `#deliver!`. with the constructor (`#initialize`) and respond to `#deliver!`.
### Multipart Delivery ### Multipart Delivery
@ -402,8 +435,8 @@ All the email are sent as multipart messages by default.
For a given mailer, the framework looks up for associated text (`.txt`) and `HTML` (`.html`) templates and render them. For a given mailer, the framework looks up for associated text (`.txt`) and `HTML` (`.html`) templates and render them.
```ruby ```ruby
InvoiceMailer.deliver # delivers both text and html templates InvoiceMailer.new(configuration: configuration).deliver({}) # delivers both text and html templates
InvoiceMailer.deliver(format: :txt) # delivers only text template InvoiceMailer.new(configuration: configuration).deliver(format: :txt) # delivers only text template
``` ```
Please note that **they aren't both mandatory, but at least one of them MUST** be present. Please note that **they aren't both mandatory, but at least one of them MUST** be present.

48
benchmark.rb Normal file
View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'bundler/setup'
require 'hanami/mailer'
require 'benchmark/ips'
require 'allocation_stats'
require_relative './examples/base'
configuration = Hanami::Mailer::Configuration.new do |config|
config.root = "examples/base"
config.delivery_method = :test
end
configuration = Hanami::Mailer.finalize(configuration)
invoice = Invoice.new(1, 23)
user = User.new("Luca", "luca@domain.test")
mailer = InvoiceMailer.new(configuration: configuration)
Benchmark.ips do |x|
# # Configure the number of seconds used during
# # the warmup phase (default 2) and calculation phase (default 5)
# x.config(time: 5, warmup: 2)
x.report "deliver" do
mailer.deliver(invoice: invoice, user: user)
end
end
stats = AllocationStats.new(burn: 5).trace do
1_000.times do
mailer.deliver(invoice: invoice, user: user)
end
end
total_allocations = stats.allocations.all.size
puts "total allocations: #{total_allocations}"
total_memsize = stats.allocations.bytes.to_a.inject(&:+)
puts "total memsize: #{total_memsize}"
detailed_allocations = stats.allocations(alias_paths: true)
.group_by(:sourcefile, :class_plus)
.sort_by_count
.to_text
puts 'allocations by source file and class:'
puts detailed_allocations

View File

@ -1,3 +0,0 @@
---
threshold: 10
total_score: 77

View File

@ -1,2 +1,2 @@
--- ---
threshold: 20.2 threshold: 18.4

View File

@ -25,12 +25,12 @@ DuplicateMethodCall:
FeatureEnvy: FeatureEnvy:
enabled: true enabled: true
exclude: exclude:
- Hanami::Mailer#build - Hanami::Mailer#__part?
LongParameterList: LongParameterList:
enabled: true enabled: true
exclude: exclude:
- Devtools::Config#self.attribute - Devtools::Config#self.attribute
max_params: 2 max_params: 4
overrides: {} overrides: {}
LongYieldList: LongYieldList:
enabled: true enabled: true
@ -44,28 +44,23 @@ NestedIterators:
NilCheck: NilCheck:
enabled: true enabled: true
exclude: exclude:
- Hanami::Mailer::Configuration#default_charset
- Hanami::Mailer::Configuration#delivery_method
- Hanami::Mailer::Dsl#bcc - Hanami::Mailer::Dsl#bcc
- Hanami::Mailer::Dsl#cc - Hanami::Mailer::Dsl#cc
- Hanami::Mailer::Dsl#from - Hanami::Mailer::Dsl#from
- Hanami::Mailer::Dsl#subject - Hanami::Mailer::Dsl#subject
- Hanami::Mailer::Dsl#template
- Hanami::Mailer::Dsl#templates
- Hanami::Mailer::Dsl#to - Hanami::Mailer::Dsl#to
- Hanami::Mailer#__part? - Hanami::Mailer#__part?
RepeatedConditional: RepeatedConditional:
enabled: true enabled: true
max_ifs: 1 max_ifs: 1
exclude: exclude: []
- Hanami::Mailer::Configuration
TooManyConstants: TooManyConstants:
enabled: true enabled: true
exclude: exclude:
- Devtools - Devtools
TooManyInstanceVariables: TooManyInstanceVariables:
enabled: true enabled: true
max_instance_variables: 2 max_instance_variables: 3
exclude: exclude:
- Hanami::Mailer::Configuration - Hanami::Mailer::Configuration
TooManyMethods: TooManyMethods:
@ -76,12 +71,10 @@ TooManyStatements:
enabled: true enabled: true
max_statements: 5 max_statements: 5
exclude: exclude:
- initialize - Hanami::Mailer#bind
- Hanami::Mailer::Configuration#duplicate - Hanami::Mailer::Configuration#initialize
- Hanami::Mailer::Dsl#self.extended - Hanami::Mailer::Dsl#self.extended
- Hanami::Mailer::Rendering::TemplatesFinder#find - Hanami::Mailer::TemplatesFinder#find
- Hanami::Mailer#build
- Hanami::Mailer#self.included
UncommunicativeMethodName: UncommunicativeMethodName:
enabled: true enabled: true
reject: reject:
@ -104,9 +97,7 @@ UncommunicativeParameterName:
- !ruby/regexp /[0-9]$/ - !ruby/regexp /[0-9]$/
- !ruby/regexp /[A-Z]/ - !ruby/regexp /[A-Z]/
accept: [] accept: []
exclude: exclude: []
- Hanami::Mailer#method_missing
- Hanami::Mailer#respond_to_missing?
UncommunicativeVariableName: UncommunicativeVariableName:
enabled: true enabled: true
reject: reject:
@ -114,10 +105,7 @@ UncommunicativeVariableName:
- !ruby/regexp /[0-9]$/ - !ruby/regexp /[0-9]$/
- !ruby/regexp /[A-Z]/ - !ruby/regexp /[A-Z]/
accept: [] accept: []
exclude: exclude: []
- Hanami::Mailer::Configuration#duplicate
- Hanami::Mailer::Configuration#load!
- Hanami::Mailer#build
UnusedParameters: UnusedParameters:
enabled: true enabled: true
exclude: [] exclude: []
@ -125,15 +113,10 @@ UtilityFunction:
enabled: true enabled: true
exclude: exclude:
- Devtools::Project::Initializer::Rspec#require_files # intentional for deduplication - Devtools::Project::Initializer::Rspec#require_files # intentional for deduplication
- Hanami::Mailer::Rendering::TemplateName#tokens
max_helper_calls: 0 max_helper_calls: 0
PrimaDonnaMethod: PrimaDonnaMethod:
exclude: exclude: []
- Hanami::Mailer::Configuration
- Hanami::Mailer::Rendering::TemplateName
ModuleInitialize: ModuleInitialize:
exclude: exclude: []
- Hanami::Mailer
InstanceVariableAssumption: InstanceVariableAssumption:
exclude: exclude: []
- Hanami::Mailer::Configuration

View File

@ -1,97 +0,0 @@
inherit_from:
- https://raw.githubusercontent.com/hanami/hanami/master/.rubocop.yml
Metrics/BlockLength:
Exclude:
# Ignore RSpec DSL
- spec/**/*
# Avoid parameter lists longer than four parameters.
ParameterLists:
Max: 4
CountKeywordArgs: true
# Avoid more than `Max` levels of nesting.
BlockNesting:
Max: 3
# Align with the style guide.
CollectionMethods:
PreferredMethods:
collect: 'map'
inject: 'reduce'
find: 'detect'
find_all: 'select'
# Disable documentation checking until a class needs to be documented once
Documentation:
Enabled: false
# Do not favor modifier if/unless usage when you have a single-line body
IfUnlessModifier:
Enabled: false
# Allow case equality operator (in limited use within the specs)
CaseEquality:
Enabled: false
# Constants do not always have to use SCREAMING_SNAKE_CASE
ConstantName:
Enabled: false
# Not all trivial readers/writers can be defined with attr_* methods
TrivialAccessors:
Enabled: false
# Allow empty lines around class body
EmptyLinesAroundClassBody:
Enabled: false
# Allow empty lines around module body
EmptyLinesAroundModuleBody:
Enabled: false
# Allow empty lines around block body
EmptyLinesAroundBlockBody:
Enabled: false
# Allow multiple line operations to not require indentation
MultilineOperationIndentation:
Enabled: false
# Prefer String#% over Kernel#sprintf
FormatString:
Enabled: false
# Align if/else blocks with the variable assignment
EndAlignment:
EnforcedStyleAlignWith: variable
# Do not always align parameters when it is easier to read
AlignParameters:
Exclude:
- spec/**/*_spec.rb
# Prefer #kind_of? over #is_a?
ClassCheck:
EnforcedStyle: kind_of?
# Do not prefer double quotes to be used when %q or %Q is more appropriate
UnneededPercentQ:
Enabled: false
# Do not prefer lambda.call(...) over lambda.(...)
LambdaCall:
Enabled: false
# Allow additional spaces
ExtraSpacing:
Enabled: false
# All objects can still be mutated if their eigenclass is patched
RedundantFreeze:
Enabled: false
# Prefer using `fail` when raising and `raise` when reraising
SignalException:
Enabled: false

46
examples/base.rb Normal file
View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'bundler/setup'
require 'hanami/mailer'
configuration = Hanami::Mailer::Configuration.new do |config|
config.root = File.expand_path(__dir__, "base")
config.delivery_method = :test
end
class Invoice
attr_reader :id, :number
def initialize(id, number)
@id = id
@number = number
freeze
end
end
class User
attr_reader :name, :email
def initialize(name, email)
@name = name
@email = email
freeze
end
end
class InvoiceMailer < Hanami::Mailer
template "invoice"
from "invoices@domain.test"
to ->(locals) { locals.fetch(:user).email }
subject ->(locals) { "Invoice ##{locals.fetch(:invoice).number}" }
end
configuration = Hanami::Mailer.finalize(configuration)
invoice = Invoice.new(1, 23)
user = User.new("Luca", "luca@domain.test")
mailer = InvoiceMailer.new(configuration: configuration)
puts mailer.deliver(invoice: invoice, user: user)

View File

@ -0,0 +1,8 @@
<html>
<head>
<title>Invoice</title>
</head>
<body>
<h1>Invoice #<%= invoice.number %></h1>
</body>
</html>

View File

@ -0,0 +1 @@
Invoice #<%= invoice.number %>

View File

@ -25,5 +25,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler', '~> 1.15' spec.add_development_dependency 'bundler', '~> 1.15'
spec.add_development_dependency 'rake', '~> 12' spec.add_development_dependency 'rake', '~> 12'
spec.add_development_dependency 'rspec', '~> 3.5' spec.add_development_dependency 'rspec', '~> 3.7'
end end

View File

@ -1 +0,0 @@
require 'hanami/mailer' # rubocop:disable Naming/FileName

View File

@ -1,8 +1,7 @@
require 'hanami/utils/class_attribute' # frozen_string_literal: true
require 'hanami/mailer/version'
require 'hanami/mailer/configuration'
require 'hanami/mailer/dsl'
require 'mail' require 'mail'
require 'concurrent'
# Hanami # Hanami
# #
@ -11,23 +10,12 @@ module Hanami
# Hanami::Mailer # Hanami::Mailer
# #
# @since 0.1.0 # @since 0.1.0
module Mailer class Mailer
# Base error for Hanami::Mailer require 'hanami/mailer/version'
# require 'hanami/mailer/template'
# @since 0.1.0 require 'hanami/mailer/finalizer'
class Error < ::StandardError require 'hanami/mailer/configuration'
end require 'hanami/mailer/dsl'
# Missing delivery data error
#
# It's raised when a mailer doesn't specify <tt>from</tt> or <tt>to</tt>.
#
# @since 0.1.0
class MissingDeliveryDataError < Error
def initialize
super("Missing delivery data, please check 'from', or 'to'")
end
end
# Content types mapping # Content types mapping
# #
@ -38,173 +26,156 @@ module Hanami
txt: 'text/plain' txt: 'text/plain'
}.freeze }.freeze
include Utils::ClassAttribute private_constant(:CONTENT_TYPES)
# @since 0.1.0 # Base error for Hanami::Mailer
# @api private
class_attribute :configuration
self.configuration = Configuration.new
# Configure the framework.
# It yields the given block in the context of the configuration
#
# @param blk [Proc] the configuration block
# #
# @since 0.1.0 # @since 0.1.0
# class Error < ::StandardError
# @see Hanami::Mailer::Configuration
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# root '/path/to/root'
# end
def self.configure(&blk)
configuration.instance_eval(&blk)
self
end end
# Unknown mailer
#
# This error is raised at the runtime when trying to deliver a mail message,
# by using a configuration that it wasn't finalized yet.
#
# @since next
# @api unstable
#
# @see Hanami::Mailer.finalize
class UnknownMailerError < Error
# @param mailer [Hanami::Mailer] a mailer
#
# @since next
# @api unstable
def initialize(mailer)
super("Unknown mailer: #{mailer.inspect}. Please finalize the configuration before to use it.")
end
end
# Missing delivery data error
#
# It's raised when a mailer doesn't specify `from` or `to`.
#
# @since 0.1.0
class MissingDeliveryDataError < Error
def initialize
super("Missing delivery data, please check 'from', or 'to'")
end
end
# @since next
# @api unstable
@_subclasses = Concurrent::Array.new
# Override Ruby's hook for modules. # Override Ruby's hook for modules.
# It includes basic Hanami::Mailer modules to the given Class. # It includes basic `Hanami::Mailer` modules to the given Class.
# It sets a copy of the framework configuration # It sets a copy of the framework configuration
# #
# @param base [Class] the target mailer # @param base [Class] the target mailer
# #
# @since 0.1.0 # @since next
# @api private # @api unstable
# def self.inherited(base)
# @see http://www.ruby-doc.org/core/Module.html#method-i-included @_subclasses.push(base)
def self.included(base) base.extend Dsl
conf = configuration
conf.add_mailer(base)
base.class_eval do
extend Dsl
extend ClassMethods
include Utils::ClassAttribute
class_attribute :configuration
self.configuration = conf.duplicate
end
conf.copy!(base)
end end
# Test deliveries private_class_method :inherited
# Finalize the configuration
# #
# This is a collection of delivered messages, used when <tt>delivery_method</tt> # This should be used before to start to use the mailers
# is set on <tt>:test</tt>
# #
# @return [Array] a collection of delivered messages # @param configuration [Hanami::Mailer::Configuration] the configuration to
# finalize
# #
# @since 0.1.0 # @return [Hanami::Mailer::Configuration] the finalized configuration
# #
# @see Hanami::Mailer::Configuration#delivery_mode # @since next
# @api unstable
# #
# @example # @example
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# Hanami::Mailer.configure do # configuration = Hanami::Mailer::Configuration.new do |config|
# delivery_method :test # # ...
# end.load! # end
# #
# # In testing code # configuration = Hanami::Mailer.finalize(configuration)
# Signup::Welcome.deliver # MyMailer.new(configuration: configuration)
# Hanami::Mailer.deliveries.count # => 1 def self.finalize(configuration)
def self.deliveries Finalizer.finalize(@_subclasses, configuration)
Mail::TestMailer.deliveries
end
# Load the framework
#
# @since 0.1.0
# @api private
def self.load!
Mail.eager_autoload!
configuration.load!
end
# @since 0.1.0
module ClassMethods
# Delivers a multipart email message.
#
# When a mailer defines a <tt>html</tt> and <tt>txt</tt> template, they are
# both delivered.
#
# In order to selectively deliver only one of the two templates, use
# <tt>Signup::Welcome.deliver(format: :txt)</tt>
#
# All the given locals, excepted the reserved ones (<tt>:format</tt> and
# <tt>charset</tt>), are available as rendering context for the templates.
#
# @param locals [Hash] a set of objects that acts as context for the rendering
# @option :format [Symbol] specify format to deliver
# @option :charset [String] charset
#
# @since 0.1.0
#
# @see Hanami::Mailer::Configuration#default_charset
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# delivery_method :smtp
# end.load!
#
# module Billing
# class Invoice
# include Hanami::Mailer
#
# from 'noreply@example.com'
# to :recipient
# subject :subject_line
#
# def prepare
# mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
# end
#
# private
#
# def recipient
# user.email
# end
#
# def subject_line
# "Invoice - #{ invoice.number }"
# end
# end
# end
#
# invoice = Invoice.new
# user = User.new(name: 'L', email: 'user@example.com')
#
# Billing::Invoice.deliver(invoice: invoice, user: user) # Deliver both text, HTML parts and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user, format: :txt) # Deliver only the text part and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user, format: :html) # Deliver only the text part and the attachment
# Billing::Invoice.deliver(invoice: invoice, user: user, charset: 'iso-8859') # Deliver both the parts with "iso-8859"
def deliver(locals = {})
new(locals).deliver
end
end end
# Initialize a mailer # Initialize a mailer
# #
# @param locals [Hash] a set of objects that acts as context for the rendering # @param configuration [Hanami::Mailer::Configuration] the configuration
# @option :format [Symbol] specify format to deliver # @return [Hanami::Mailer]
# @option :charset [String] charset
# #
# @since 0.1.0 # @since 0.1.0
def initialize(locals = {}) def initialize(configuration:)
@locals = locals @configuration = configuration
@format = locals.fetch(:format, nil) freeze
@charset = locals.fetch(:charset, self.class.configuration.default_charset)
@mail = build
prepare
end end
# Prepare the email message when a new mailer is initialized.
#
# @return [Mail::Message] the delivered email
#
# @since 0.1.0
# @api unstable
#
# @see Hanami::Mailer::Configuration#default_charset
#
# @example
# require 'hanami/mailer'
#
# configuration = Hanami::Mailer::Configuration.new do |config|
# config.delivery_method = :smtp
# end
#
# configuration = Hanami::Mailer.finalize(configuration)
#
# module Billing
# class InvoiceMailer < Hanami::Mailer
# from 'noreply@example.com'
# to ->(locals) { locals.fetch(:user).email }
# subject ->(locals) { "Invoice number #{locals.fetch(:invoice).number}" }
#
# before do |mail, locals|
# mail.attachments["invoice-#{locals.fetch(:invoice).number}.pdf"] =
# File.read('/path/to/invoice.pdf')
# end
# end
# end
#
# invoice = Invoice.new(number: 23)
# user = User.new(name: 'L', email: 'user@example.com')
#
# mailer = Billing::InvoiceMailer.new(configuration: configuration)
#
# # Deliver both text, HTML parts and the attachment
# mailer.deliver(invoice: invoice, user: user)
#
# # Deliver only the text part and the attachment
# mailer.deliver(invoice: invoice, user: user, format: :txt)
#
# # Deliver only the text part and the attachment
# mailer.deliver(invoice: invoice, user: user, format: :html)
#
# # Deliver both the parts with "iso-8859"
# mailer.deliver(invoice: invoice, user: user, charset: 'iso-8859')
def deliver(locals)
mail(locals).deliver
rescue ArgumentError
raise MissingDeliveryDataError
end
# @since next
# @api unstable
alias call deliver
# Render a single template with the specified format. # Render a single template with the specified format.
# #
# @param format [Symbol] format # @param format [Symbol] format
@ -212,127 +183,78 @@ module Hanami
# @return [String] the output of the rendering process. # @return [String] the output of the rendering process.
# #
# @since 0.1.0 # @since 0.1.0
# @api private # @api unstable
def render(format) def render(format, locals)
self.class.templates(format).render(self, @locals) template(format).render(self, locals)
end end
# Delivers a multipart email, by looking at all the associated templates and render them.
#
# @since 0.1.0
# @api private
def deliver
mail.deliver
rescue ArgumentError => exception
raise MissingDeliveryDataError if exception.message =~ /SMTP (From|To) address/
raise
end
protected
# Prepare the email message when a new mailer is initialized.
#
# This is a hook that can be overwritten by mailers.
#
# @since 0.1.0
#
# @example
# require 'hanami/mailer'
#
# module Billing
# class Invoice
# include Hanami::Mailer
#
# subject 'Invoice'
# from 'noreply@example.com'
# to ''
#
# def prepare
# mail.attachments['invoice.pdf'] = File.read('/path/to/invoice.pdf')
# end
#
# private
#
# def recipient
# user.email
# end
# end
# end
#
# invoice = Invoice.new
# user = User.new(name: 'L', email: 'user@example.com')
def prepare
end
# @api private
# @since 0.1.0
def method_missing(m)
@locals.fetch(m) { super }
end
# @since 0.1.0
attr_reader :mail
# @api private
# @since 0.1.0
attr_reader :charset
private private
# rubocop:disable Metrics/MethodLength # @api unstable
# rubocop:disable Metrics/AbcSize # @since next
# @api private attr_reader :configuration
def build
Mail.new.tap do |m|
m.from = __dsl(:from)
m.to = __dsl(:to)
m.cc = __dsl(:cc)
m.bcc = __dsl(:bcc)
m.subject = __dsl(:subject)
m.charset = charset # @api unstable
m.html_part = __part(:html) # @since next
m.text_part = __part(:txt) def mail(locals)
Mail.new.tap do |mail|
m.delivery_method(*Hanami::Mailer.configuration.delivery_method) instance_exec(mail, locals, &self.class.before)
bind(mail, locals)
end end
end end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# @api private # @api unstable
# @since next
def bind(mail, locals) # rubocop:disable Metrics/AbcSize
charset = locals.fetch(:charset, configuration.default_charset)
mail.from = __dsl(:from, locals)
mail.to = __dsl(:to, locals)
mail.cc = __dsl(:cc, locals)
mail.bcc = __dsl(:bcc, locals)
mail.subject = __dsl(:subject, locals)
mail.html_part = __part(:html, charset, locals)
mail.text_part = __part(:txt, charset, locals)
mail.charset = charset
mail.delivery_method(*configuration.delivery_method)
end
# @since next
# @api unstable
def template(format)
configuration.template(self.class, format)
end
# @since 0.1.0 # @since 0.1.0
def __dsl(method_name) # @api unstable
def __dsl(method_name, locals)
case result = self.class.__send__(method_name) case result = self.class.__send__(method_name)
when Symbol when Proc
__send__(result) result.call(locals)
else else
result result
end end
end end
# @api private
# @since 0.1.0 # @since 0.1.0
def __part(format) # @api unstable
return unless __part?(format) def __part(format, charset, locals)
return unless __part?(format, locals)
Mail::Part.new.tap do |part| Mail::Part.new.tap do |part|
part.content_type = "#{CONTENT_TYPES.fetch(format)}; charset=#{charset}" part.content_type = "#{CONTENT_TYPES.fetch(format)}; charset=#{charset}"
part.body = render(format) part.body = render(format, locals)
end end
end end
# @api private
# @since 0.1.0 # @since 0.1.0
def __part?(format) # @api unstable
@format == format || def __part?(format, locals)
(!@format && !self.class.templates(format).nil?) wanted = locals.fetch(:format, nil)
end wanted == format ||
(!wanted && !template(format).nil?)
# @api private
# @since 0.4.0
def respond_to_missing?(m, _include_all)
@locals.key?(m)
end end
end end
end end

View File

@ -1,8 +1,11 @@
require 'set' # frozen_string_literal: true
require 'hanami/utils/kernel' require 'hanami/utils/kernel'
require 'hanami/mailer/template_name'
require 'hanami/mailer/templates_finder'
module Hanami module Hanami
module Mailer class Mailer
# Framework configuration # Framework configuration
# #
# @since 0.1.0 # @since 0.1.0
@ -11,7 +14,7 @@ module Hanami
# #
# @since 0.1.0 # @since 0.1.0
# @api private # @api private
DEFAULT_ROOT = '.'.freeze DEFAULT_ROOT = '.'
# Default delivery method # Default delivery method
# #
@ -23,24 +26,33 @@ module Hanami
# #
# @since 0.1.0 # @since 0.1.0
# @api private # @api private
DEFAULT_CHARSET = 'UTF-8'.freeze DEFAULT_CHARSET = 'UTF-8'
# @since 0.1.0 private_constant(*constants(false))
# @api private
attr_reader :mailers
# @since 0.1.0
# @api private
attr_reader :modules
# Initialize a configuration instance # Initialize a configuration instance
# #
# @yield [config] the new initialized configuration instance
# @return [Hanami::Mailer::Configuration] a new configuration's instance # @return [Hanami::Mailer::Configuration] a new configuration's instance
# #
# @since 0.1.0 # @since 0.1.0
#
# @example Basic Usage
# require 'hanami/mailer'
#
# configuration = Hanami::Mailer::Configuration.new do |config|
# config.delivery_method :smtp, ...
# end
def initialize def initialize
@namespace = Object @mailers = {}
reset!
self.namespace = Object
self.root = DEFAULT_ROOT
self.delivery_method = DEFAULT_DELIVERY_METHOD
self.default_charset = DEFAULT_CHARSET
yield(self) if block_given?
@finder = TemplatesFinder.new(root)
end end
# Set the Ruby namespace where to lookup for mailers. # Set the Ruby namespace where to lookup for mailers.
@ -49,91 +61,52 @@ module Hanami
# that if a `MyApp` wants a `Mailers::Signup` mailer, we are loading the # that if a `MyApp` wants a `Mailers::Signup` mailer, we are loading the
# right one. # right one.
# #
# If not set, this value defaults to `Object`. # @!attribute namespace
# @return [Class,Module,String] the Ruby namespace where the mailers
# are located
# #
# This is part of a DSL, for this reason when this method is called with # @since next
# an argument, it will set the corresponding instance variable. When # @api unstable
# called without, it will return the already set value, or the default.
# #
# @overload namespace(value) # @example
# Sets the given value
# @param value [Class, Module, String] a valid Ruby namespace identifier
#
# @overload namespace
# Gets the value
# @return [Class, Module, String]
#
# @api private
# @since 0.1.0
#
# @example Getting the value
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# Hanami::Mailer.configuration.namespace # => Object # Hanami::Mailer::Configuration.new do |config|
# # config.namespace = MyApp::Mailers
# @example Setting the value
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# namespace 'MyApp::Mailers'
# end # end
def namespace(value = nil) attr_accessor :namespace
if value
@namespace = value
else
@namespace
end
end
# Set the root path where to search for templates # Set the root path where to search for templates
# #
# If not set, this value defaults to the current directory. # If not set, this value defaults to the current directory.
# #
# When this method is called with an argument, it will set the corresponding instance variable. # @param value [String, Pathname] the root path for mailer templates
# When called without, it will return the already set value, or the default.
# #
# @overload root(value) # @raise [Errno::ENOENT] if the path doesn't exist
# Sets the given value
# @param value [String, Pathname, #to_pathname] an object that can be
# coerced to Pathname
# #
# @overload root # @since next
# Gets the value # @api unstable
# @return [Pathname]
#
# @since 0.1.0
# #
# @see http://www.ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html # @see http://www.ruby-doc.org/stdlib/libdoc/pathname/rdoc/Pathname.html
# @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Pathname-class_method # @see http://rdoc.info/gems/hanami-utils/Hanami/Utils/Kernel#Pathname-class_method
# #
# @example Getting the value # @example
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# Hanami::Mailer.configuration.root # => #<Pathname:.> # Hanami::Mailer::Configuration.new do |config|
# # config.root = 'path/to/templates'
# @example Setting the value
# require 'hanami/mailer'
#
# Hanami::Mailer.configure do
# root '/path/to/templates'
# end # end
# def root=(value)
# Hanami::Mailer.configuration.root # => #<Pathname:/path/to/templates> @root = Utils::Kernel.Pathname(value).realpath
def root(value = nil)
if value
@root = Utils::Kernel.Pathname(value).realpath
else
@root
end
end end
# Prepare the mailers. # @!attribute [r] root
# # @return [Pathname] the root path for mailer templates
# The given block will be yielded when `Hanami::Mailer` will be included by
# a mailer.
#
# This method can be called multiple times.
# #
# @since next
# @api unstable
attr_reader :root
# @param blk [Proc] the code block # @param blk [Proc] the code block
# #
# @return [void] # @return [void]
@ -144,19 +117,8 @@ module Hanami
# #
# @see Hanami::Mailer.configure # @see Hanami::Mailer.configure
def prepare(&blk) def prepare(&blk)
if block_given? # rubocop:disable Style/GuardClause raise ArgumentError.new('Please provide a block') unless block_given?
@modules.push(blk) @modules.push(blk)
else
raise ArgumentError.new('Please provide a block')
end
end
# Add a mailer to the registry
#
# @since 0.1.0
# @api private
def add_mailer(mailer)
@mailers.add(mailer)
end end
# Duplicate by copying the settings in a new instance. # Duplicate by copying the settings in a new instance.
@ -211,40 +173,41 @@ module Hanami
# #
# It supports the following delivery methods: # It supports the following delivery methods:
# #
# * Exim (<tt>:exim</tt>) # * Exim (`:exim`)
# * Sendmail (<tt>:sendmail</tt>) # * Sendmail (`:sendmail`)
# * SMTP (<tt>:smtp</tt>, for local installations) # * SMTP (`:smtp`, for local installations)
# * SMTP Connection (<tt>:smtp_connection</tt>, # * SMTP Connection (`:smtp_connection`,
# via <tt>Net::SMTP</tt> - for remote installations) # via `Net::SMTP` - for remote installations)
# * Test (<tt>:test</tt>, for testing purposes) # * Test (`:test`, for testing purposes)
# #
# The default delivery method is SMTP (<tt>:smtp</tt>). # The default delivery method is SMTP (`:smtp`).
# #
# Custom delivery methods can be specified by passing the class policy and # Custom delivery methods can be specified by passing the class policy and
# a set of optional configurations. This class MUST respond to: # a set of optional configurations. This class MUST respond to:
# #
# * <tt>initialize(options = {})</tt> # * `initialize(options = {})`
# * <tt>deliver!(mail)<tt> # * `deliver!(mail)`
# #
# @param method [Symbol, #initialize, deliver!] delivery method # @param method [Symbol, #initialize, deliver!] delivery method
# @param options [Hash] optional settings # @param options [Hash] optional settings
# #
# @return [Array] an array containing the delivery method and the optional settings as an Hash # @return [Array] an array containing the delivery method and the optional settings as an Hash
# #
# @since 0.1.0 # @since next
# @api unstable
# #
# @example Setup delivery method with supported symbol # @example Setup delivery method with supported symbol
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# Hanami::Mailer.configure do # Hanami::Mailer::Configuration.new do |config|
# delivery_method :sendmail # config.delivery_method = :sendmail
# end # end
# #
# @example Setup delivery method with supported symbol and options # @example Setup delivery method with supported symbol and options
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# Hanami::Mailer.configure do # Hanami::Mailer::Configuration.new do |config|
# delivery_method :smtp, address: "localhost", port: 1025 # config.delivery_method = :smtp, address: "localhost", port: 1025
# end # end
# #
# @example Setup custom delivery method with options # @example Setup custom delivery method with options
@ -260,49 +223,77 @@ module Hanami
# end # end
# end # end
# #
# Hanami::Mailer.configure do # Hanami::Mailer.Configuration.new do |config|
# delivery_method MandrillDeliveryMethod, # config.delivery_method = MandrillDeliveryMethod,
# username: ENV['MANDRILL_USERNAME'], # username: ENV['MANDRILL_USERNAME'],
# password: ENV['MANDRILL_API_KEY'] # password: ENV['MANDRILL_API_KEY']
# end # end
def delivery_method(method = nil, options = {}) attr_accessor :delivery_method
if method.nil?
@delivery_method # Specify a default charset for all the delivered emails
else #
@delivery_method = [method, options] # If not set, it defaults to `UTF-8`
end #
# @!attribute default_charset
# @return [String] the charset
#
# @since next
# @api unstable
#
# @example
# require 'hanami/mailer'
#
# Hanami::Mailer::Configuration.new do |config|
# config.default_charset = "iso-8859-1"
# end
attr_accessor :default_charset
# Add a mailer to the registry
#
# @param mailer [Hanami::Mailer] a mailer
#
# @since 0.1.0
# @api unstable
def add_mailer(mailer)
template_name = TemplateName[mailer.template_name, namespace]
templates = finder.find(template_name)
mailers[mailer] = templates
end end
# @since 0.1.0 # @param mailer [Hanami::Mailer] a mailer
def default_charset(value = nil) # @param format [Symbol] the wanted format (eg. `:html`, `:txt`)
if value.nil? #
@default_charset # @raise [Hanami::Mailer::UnknownMailerError] if the given mailer is not
else # present in the configuration. This happens when the configuration is
@default_charset = value # used before to being finalized.
end #
# @since next
# @api unstable
def template(mailer, format)
mailers.fetch(mailer) { raise UnknownMailerError.new(mailer) }[format]
end end
protected # Deep freeze the important instance variables
#
# @since next
# @api unstable
def freeze
delivery_method.freeze
default_charset.freeze
mailers.freeze
super
end
# @api private private
# @since 0.1.0
attr_writer :root
# @api private
# @since 0.1.0 # @since 0.1.0
attr_writer :delivery_method # @api private
attr_reader :mailers
# @api private # @since next
# @since 0.1.0 # @api unstable
attr_writer :default_charset attr_reader :finder
# @api private
# @since 0.1.0
attr_writer :namespace
# @api private
# @since 0.1.0
attr_writer :modules
end end
end end
end end

View File

@ -1,89 +1,31 @@
require 'hanami/mailer/rendering/template_name' # frozen_string_literal: true
require 'hanami/mailer/rendering/templates_finder'
module Hanami module Hanami
module Mailer # Hanami::Mailer
#
# @since 0.1.0
class Mailer
require 'hanami/mailer/template_name'
# Class level DSL # Class level DSL
# #
# @since 0.1.0 # @since 0.1.0
module Dsl module Dsl
# @since 0.3.0 # @since 0.3.0
# @api private # @api unstable
def self.extended(base) def self.extended(base)
base.class_eval do base.class_eval do
@from = nil @from = nil
@to = nil @to = nil
@cc = nil @cc = nil
@bcc = nil @bcc = nil
@subject = nil @subject = nil
@template = nil
@before = ->(*) {}
end end
end end
# Set the template name IF it differs from the convention. private_class_method :extended
#
# For a given mailer named <tt>Signup::Welcome</tt> it will look for
# <tt>signup/welcome.*.*</tt> templates under the root directory.
#
# If for some reason, we need to specify a different template name, we can
# use this method.
#
# This is part of a DSL, for this reason when this method is called with
# an argument, it will set the corresponding class variable. When
# called without, it will return the already set value, or the default.
#
# @overload template(value)
# Sets the given value
# @param value [String, #to_s] relative template path, under root
# @return [NilClass]
#
# @overload template
# Gets the template name
# @return [String]
#
# @since 0.1.0
#
# @see Hanami::Mailers::Configuration.root
#
# @example Custom template name
# require 'hanami/mailer'
#
# class MyMailer
# include Hanami::Mailer
# template 'mailer'
# end
def template(value = nil)
if value.nil?
@template ||= ::Hanami::Mailer::Rendering::TemplateName.new(name, configuration.namespace).to_s
else
@template = value
end
end
# Returns a set of associated templates or only one for the given format
#
# This is part of a DSL, for this reason when this method is called with
# an argument, it will set the corresponding class variable. When
# called without, it will return the already set value, or the default.
#
# @overload templates(format)
# Returns the template associated with the given format
# @param value [Symbol] the format
# @return [Hash]
#
# @overload templates
# Returns all the associated templates
# Gets the template name
# @return [Hash] a set of templates
#
# @since 0.1.0
# @api private
def templates(format = nil)
if format.nil?
@templates = ::Hanami::Mailer::Rendering::TemplatesFinder.new(self).find
else
@templates.fetch(format, nil)
end
end
# Sets the sender for mail messages # Sets the sender for mail messages
# #
@ -114,24 +56,15 @@ module Hanami
# @example Hardcoded value (String) # @example Hardcoded value (String)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# from "noreply@example.com" # from "noreply@example.com"
# end # end
# #
# @example Method (Symbol) # @example Lazy (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # from ->(locals) { locals.fetch(:sender).email }
# from :sender
#
# private
#
# def sender
# "noreply@example.com"
# end
# end # end
def from(value = nil) def from(value = nil)
if value.nil? if value.nil?
@ -141,6 +74,74 @@ module Hanami
end end
end end
# Sets the recipient for mail messages
#
# It accepts a hardcoded value as a string or array of strings.
# For dynamic values, you can specify a symbol that represents an instance
# method.
#
# This value MUST be set, otherwise an exception is raised at the delivery
# time.
#
# When a value is given, specify the recipient of the email
# Otherwise, it returns the recipient of the email
#
# This is part of a DSL, for this reason when this method is called with
# an argument, it will set the corresponding class variable. When
# called without, it will return the already set value, or the default.
#
# @overload to(value)
# Sets the recipient
# @param value [String, Array, Symbol] the hardcoded value or method name
# @return [NilClass]
#
# @overload to
# Returns the recipient
# @return [String, Array, Symbol] the recipient
#
# @since 0.1.0
#
# @example Hardcoded value (String)
# require 'hanami/mailer'
#
# class WelcomeMailer < Hanami::Mailer
# to "user@example.com"
# end
#
# @example Hardcoded value (Array)
# require 'hanami/mailer'
#
# class WelcomeMailer < Hanami::Mailer
# to ["user-1@example.com", "user-2@example.com"]
# end
#
# @example Lazy value (Proc)
# require 'hanami/mailer'
#
# class WelcomeMailer < Hanami::Mailer
# to ->(locals) { locals.fetch(:user).email }
# end
#
# user = User.new(name: 'L')
# WelcomeMailer.new(configuration: configuration).deliver(user: user)
#
# @example Lazy values (Proc)
# require 'hanami/mailer'
#
# class WelcomeMailer < Hanami::Mailer
# to ->(locals) { locals.fetch(:users).map(&:email) }
# end
#
# users = [User.new(name: 'L'), User.new(name: 'MG')]
# WelcomeMailer.new(configuration: configuration).deliver(users: users)
def to(value = nil)
if value.nil?
@to
else
@to = value
end
end
# Sets the cc (carbon copy) for mail messages # Sets the cc (carbon copy) for mail messages
# #
# It accepts a hardcoded value as a string or array of strings. # It accepts a hardcoded value as a string or array of strings.
@ -170,58 +171,36 @@ module Hanami
# @example Hardcoded value (String) # @example Hardcoded value (String)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# to "user@example.com"
# cc "other.user@example.com" # cc "other.user@example.com"
# end # end
# #
# @example Hardcoded value (Array) # @example Hardcoded value (Array)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# to ["user-1@example.com", "user-2@example.com"]
# cc ["other.user-1@example.com", "other.user-2@example.com"] # cc ["other.user-1@example.com", "other.user-2@example.com"]
# end # end
# #
# @example Method (Symbol) # @example Lazy value (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # cc ->(locals) { locals.fetch(:user).email }
# to "user@example.com"
# cc :email_address
#
# private
#
# def email_address
# user.email
# end
# end # end
# #
# other_user = User.new(name: 'L') # user = User.new(name: 'L')
# WelcomeMailer.deliver(user: other_user) # WelcomeMailer.new(configuration: configuration).deliver(user: user)
# #
# @example Method that returns a collection of recipients # @example Lazy values (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # cc ->(locals) { locals.fetch(:users).map(&:email) }
# to "user@example.com"
# cc :recipients
#
# private
#
# def recipients
# users.map(&:email)
# end
# end # end
# #
# other_users = [User.new(name: 'L'), User.new(name: 'MG')] # users = [User.new(name: 'L'), User.new(name: 'MG')]
# WelcomeMailer.deliver(users: other_users) # WelcomeMailer.new(configuration: configuration).deliver(users: users)
def cc(value = nil) def cc(value = nil)
if value.nil? if value.nil?
@cc @cc
@ -259,58 +238,36 @@ module Hanami
# @example Hardcoded value (String) # @example Hardcoded value (String)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# to "user@example.com"
# bcc "other.user@example.com" # bcc "other.user@example.com"
# end # end
# #
# @example Hardcoded value (Array) # @example Hardcoded value (Array)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# to ["user-1@example.com", "user-2@example.com"]
# bcc ["other.user-1@example.com", "other.user-2@example.com"] # bcc ["other.user-1@example.com", "other.user-2@example.com"]
# end # end
# #
# @example Method (Symbol) # @example Lazy value (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # bcc ->(locals) { locals.fetch(:user).email }
# to "user@example.com"
# bcc :email_address
#
# private
#
# def email_address
# user.email
# end
# end # end
# #
# other_user = User.new(name: 'L') # user = User.new(name: 'L')
# WelcomeMailer.deliver(user: other_user) # WelcomeMailer.new(configuration: configuration).deliver(user: user)
# #
# @example Method that returns a collection of recipients # @example Lazy values (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # bcc ->(locals) { locals.fetch(:users).map(&:email) }
# to "user@example.com"
# bcc :recipients
#
# private
#
# def recipients
# users.map(&:email)
# end
# end # end
# #
# other_users = [User.new(name: 'L'), User.new(name: 'MG')] # users = [User.new(name: 'L'), User.new(name: 'MG')]
# WelcomeMailer.deliver(users: other_users) # WelcomeMailer.new(configuration: configuration).deliver(users: users)
def bcc(value = nil) def bcc(value = nil)
if value.nil? if value.nil?
@bcc @bcc
@ -319,92 +276,6 @@ module Hanami
end end
end end
# Sets the recipient for mail messages
#
# It accepts a hardcoded value as a string or array of strings.
# For dynamic values, you can specify a symbol that represents an instance
# method.
#
# This value MUST be set, otherwise an exception is raised at the delivery
# time.
#
# When a value is given, specify the recipient of the email
# Otherwise, it returns the recipient of the email
#
# This is part of a DSL, for this reason when this method is called with
# an argument, it will set the corresponding class variable. When
# called without, it will return the already set value, or the default.
#
# @overload to(value)
# Sets the recipient
# @param value [String, Array, Symbol] the hardcoded value or method name
# @return [NilClass]
#
# @overload to
# Returns the recipient
# @return [String, Array, Symbol] the recipient
#
# @since 0.1.0
#
# @example Hardcoded value (String)
# require 'hanami/mailer'
#
# class WelcomeMailer
# include Hanami::Mailer
#
# to "user@example.com"
# end
#
# @example Hardcoded value (Array)
# require 'hanami/mailer'
#
# class WelcomeMailer
# include Hanami::Mailer
#
# to ["user-1@example.com", "user-2@example.com"]
# end
#
# @example Method (Symbol)
# require 'hanami/mailer'
#
# class WelcomeMailer
# include Hanami::Mailer
# to :email_address
#
# private
#
# def email_address
# user.email
# end
# end
#
# user = User.new(name: 'L')
# WelcomeMailer.deliver(user: user)
#
# @example Method that returns a collection of recipients
# require 'hanami/mailer'
#
# class WelcomeMailer
# include Hanami::Mailer
# to :recipients
#
# private
#
# def recipients
# users.map(&:email)
# end
# end
#
# users = [User.new(name: 'L'), User.new(name: 'MG')]
# WelcomeMailer.deliver(users: users)
def to(value = nil)
if value.nil?
@to
else
@to = value
end
end
# Sets the subject for mail messages # Sets the subject for mail messages
# #
# It accepts a hardcoded value as a string, or a symbol that represents # It accepts a hardcoded value as a string, or a symbol that represents
@ -431,28 +302,19 @@ module Hanami
# @example Hardcoded value (String) # @example Hardcoded value (String)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer
#
# subject "Welcome" # subject "Welcome"
# end # end
# #
# @example Method (Symbol) # @example Lazy value (Proc)
# require 'hanami/mailer' # require 'hanami/mailer'
# #
# class WelcomeMailer # class WelcomeMailer < Hanami::Mailer
# include Hanami::Mailer # subject ->(locals) { "Hello #{locals.fetch(:user).name}" }
# subject :greeting
#
# private
#
# def greeting
# "Hello, #{ user.name }"
# end
# end # end
# #
# user = User.new(name: 'L') # user = User.new(name: 'L')
# WelcomeMailer.deliver(user: user) # WelcomeMailer.new(configuration: configuration).deliver(user: user)
def subject(value = nil) def subject(value = nil)
if value.nil? if value.nil?
@subject @subject
@ -461,17 +323,70 @@ module Hanami
end end
end end
protected # Set the template name **IF** it differs from the naming convention.
#
# Loading mechanism hook. # For a given mailer named `Signup::Welcome` it will look for
# `signup/welcome.*.*` templates under the root directory.
#
# If for some reason, we need to specify a different template name, we can
# use this method.
#
# @param value [String] the template name
# #
# @api private
# @since 0.1.0 # @since 0.1.0
# @api unstable
# #
# @see Hanami::Mailer.load! # @example Custom template name
def load! # require 'hanami/mailer'
templates.freeze #
configuration.freeze # class MyMailer < Hanami::Mailer
# template 'mailer'
# end
def template(value)
@template = value
end
# @since next
# @api unstable
def template_name
@template || name
end
# Before callback for email delivery
#
# @since next
# @api unstable
#
# @example
# require 'hanami/mailer'
#
# module Billing
# class InvoiceMailer < Hanami::Mailer
# subject 'Invoice'
# from 'noreply@example.com'
# to ->(locals) { locals.fetch(:user).email }
#
# before do |mail, locals|
# user = locals.fetch(:user)
# mail.attachments["invoice-#{invoice_code}-#{user.id}.pdf"] = File.read('/path/to/invoice.pdf')
# end
#
# def invoice_code
# "123"
# end
# end
# end
#
# invoice = Invoice.new
# user = User.new(name: 'L', email: 'user@example.com')
#
# InvoiceMailer.new(configuration: configuration).deliver(invoice: invoice, user: user)
def before(&blk)
if block_given?
@before = blk
else
@before
end
end end
end end
end end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require 'mail'
# require 'ice_nine'
module Hanami
class Mailer
# @since next
# @api unstable
class Finalizer
# Finalize the given configuration before to start to use the mailers
#
# @param mailers [Array<Hanami::Mailer>] all the subclasses of
# `Hanami::Mailer`
# @param configuration [Hanami::Mailer::Configuration] the configuration
# to finalize
#
# @return configuration [Hanami::Mailer::Configuration] the finalized
# configuration
#
# @since next
# @api unstable
def self.finalize(mailers, configuration)
Mail.eager_autoload!
mailers.each { |mailer| configuration.add_mailer(mailer) }
configuration.freeze
configuration
end
end
end
end

View File

@ -1,53 +0,0 @@
require 'hanami/utils/string'
module Hanami
module Mailer
module Rendering
# @since 0.1.0
# @api private
#
# TODO this is identical to Hanami::View, consider to move into Hanami::Utils
class TemplateName
# @since 0.1.0
# @api private
NAMESPACE_SEPARATOR = '::'.freeze
# @since 0.1.0
# @api private
def initialize(name, namespace)
@name = name
compile!(namespace)
end
# @since 0.1.0
# @api private
def to_s
@name
end
private
# @since 0.1.0
# @api private
def compile!(namespace)
tokens(namespace) { |token| replace!(token) }
@name = Utils::String.underscore(@name)
end
# @since 0.1.0
# @api private
def tokens(namespace)
namespace.to_s.split(NAMESPACE_SEPARATOR).each do |token|
yield token
end
end
# @since 0.1.0
# @api private
def replace!(token)
@name.gsub!(/\A#{token}#{NAMESPACE_SEPARATOR}/, '')
end
end
end
end
end

View File

@ -1,133 +0,0 @@
require 'hanami/mailer/template'
module Hanami
module Mailer
module Rendering
# Find templates for a mailer
#
# @api private
# @since 0.1.0
#
# @see Mailer::Template
class TemplatesFinder
# Default format
#
# @api private
# @since 0.1.0
FORMAT = '*'.freeze
# Default template engines
#
# @api private
# @since 0.1.0
ENGINES = '*'.freeze
# Recursive pattern
#
# @api private
# @since 0.1.0
RECURSIVE = '**'.freeze
# Initialize a finder
#
# @param mailer [Class] the mailer class
#
# @api private
# @since 0.1.0
def initialize(mailer)
@mailer = mailer
end
# Find all the associated templates to the mailer.
# It recursively looks for templates under the root path of the mailer,
# that are matching the template name
#
# @return [Hash] the templates
#
# @api private
# @since 0.1.0
#
# @see Hanami::Mailer::Dsl#root
# @see Hanami::Mailer::Dsl#templates
#
# @example
# require 'hanami/mailer'
#
# module Mailers
# class Welcome
# include Hanami::Mailer
# end
# end
#
# Mailers::Welcome.root # => "/path/to/templates"
# Mailers::Welcome.templates # => {[:html] => "welcome"}
#
# # This mailer has a template:
# #
# # "/path/to/templates/welcome.html.erb"
#
# Hanami::Mailer::Rendering::TemplatesFinder.new(Mailers::Welcome).find
# # => [#<Hanami::Mailer::Template:0x007f8a0a86a970 ... @file="/path/to/templates/welcome.html.erb">]
def find
templates = Hash[]
_find.map do |template|
name = File.basename(template)
format = (name.split('.')[-2]).to_sym
templates[format] = Mailer::Template.new(template)
end
templates
end
protected
# @api private
# @since 0.1.0
def _find(lookup = search_path)
Dir.glob("#{[root, lookup, template_name].join(separator)}.#{format}.#{engines}")
end
# @api private
# @since 0.1.0
def template_name
Rendering::TemplateName.new(@mailer.template, @mailer.configuration.namespace).to_s
end
# @api private
# @since 0.1.0
def root
@mailer.configuration.root
end
# @api private
# @since 0.1.0
def search_path
recursive
end
# @api private
# @since 0.1.0
def recursive
RECURSIVE
end
# @api private
# @since 0.1.0
def separator
::File::SEPARATOR
end
# @api private
# @since 0.1.0
def format
FORMAT
end
# @api private
# @since 0.1.0
def engines
ENGINES
end
end
end
end
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'tilt' require 'tilt'
module Hanami module Hanami
module Mailer class Mailer
# A logic-less template. # A logic-less template.
# #
# @api private # @api private
@ -11,29 +13,20 @@ module Hanami
class Template class Template
def initialize(template) def initialize(template)
@_template = Tilt.new(template) @_template = Tilt.new(template)
freeze
end end
# Render the template within the context of the given scope. # Render the template within the context of the given scope.
# #
# @param scope [Class] the rendering scope # @param scope [Object] the rendering scope
# @param locals [Hash] set of objects passed to the constructor # @param locals [Hash] set of objects passed to the constructor
# #
# @return [String] the output of the rendering process # @return [String] the output of the rendering process
# #
# @api private # @api private
# @since 0.1.0 # @since 0.1.0
def render(scope = Object.new, locals = {}) def render(scope, locals = {})
@_template.render(scope, locals) @_template.render(scope.dup, locals)
end
# Get the path to the template
#
# @return [String] the pathname
#
# @api private
# @since 0.1.0
def file
@_template.file
end end
end end
end end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
require 'hanami/utils/string'
module Hanami
class Mailer
# @since 0.1.0
# @api private
#
# TODO this is identical to Hanami::View, consider to move into Hanami::Utils
class TemplateName
# @since next
# @api unstable
def self.call(name, namespace)
Utils::String.new(name.gsub(/\A#{namespace}(::)*/, '')).underscore.to_s
end
class << self
# @since next
# @api unstable
alias [] call
end
end
end
end

View File

@ -0,0 +1,152 @@
# frozen_string_literal: true
require 'hanami/mailer/template'
require 'hanami/utils/file_list'
require 'pathname'
module Hanami
class Mailer
# Find templates for a mailer
#
# @api private
# @since 0.1.0
#
# @see Mailer::Template
class TemplatesFinder
# Default format
#
# @api private
# @since 0.1.0
FORMAT = '*'
# Default template engines
#
# @api private
# @since 0.1.0
ENGINES = '*'
# Recursive pattern
#
# @api private
# @since 0.1.0
RECURSIVE = '**'
# Format separator
#
# @api unstable
# @since next
#
# @example
# welcome.html.erb
FORMAT_SEPARATOR = '.'
private_constant(*constants(true))
# Initialize a finder
#
# @param root [String,Pathname] the root directory where to recursively
# look for templates
#
# @raise [Errno::ENOENT] if the directory doesn't exist
#
# @api unstable
# @since 0.1.0
def initialize(root)
@root = Pathname.new(root).realpath
freeze
end
# Find all the associated templates to the mailer.
#
# It starts under the root path and it **recursively** looks for templates
# that are matching the given template name.
#
# @param template_name [String] the template name
#
# @return [Hash] the templates
#
# @api unstable
# @since 0.1.0
#
# @example
# require 'hanami/mailer'
#
# module Mailers
# class Welcome < Hanami::Mailer
# end
# end
#
# configuration = Hanami::Mailer::Configuration.new do |config|
# config.root = "path/to/templates"
# end
#
# # This mailer has a template:
# #
# # "path/to/templates/welcome.html.erb"
#
# Hanami::Mailer::Rendering::TemplatesFinder.new(root).find("welcome")
# # => [#<Hanami::Mailer::Template:0x007f8a0a86a970 ... @file="path/to/templates/welcome.html.erb">]
def find(template_name)
templates(template_name).each_with_object({}) do |template, result|
format = extract_format(template)
result[format] = Mailer::Template.new(template)
end
end
protected
# @api unstable
# @since 0.1.0
def templates(template_name, lookup = search_path)
Utils::FileList["#{[root, lookup, template_name].join(separator)}#{format_separator}#{format}#{format_separator}#{engines}"]
end
# @api unstable
# @since 0.1.0
attr_reader :root
# @api private
# @since 0.1.0
def search_path
recursive
end
# @api private
# @since 0.1.0
def recursive
RECURSIVE
end
# @api private
# @since 0.1.0
def separator
::File::SEPARATOR
end
# @api private
# @since 0.1.0
def format
FORMAT
end
# @api private
# @since 0.1.0
def engines
ENGINES
end
# @api unstable
# @since next
def format_separator
FORMAT_SEPARATOR
end
# @api unstable
# @since next
def extract_format(template)
filename = File.basename(template)
filename.split(format_separator)[-2].to_sym
end
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
module Hanami module Hanami
module Mailer class Mailer
# @since 0.1.0 # @since 0.1.0
VERSION = '1.1.0'.freeze VERSION = '1.1.0'
end end
end end

View File

@ -0,0 +1,154 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer do
describe '.deliver' do
it 'can deliver with specified charset' do
mail = CharsetMailer.new(configuration: configuration).deliver(charset: charset = 'iso-2022-jp')
expect(mail.charset).to eq(charset)
expect(mail.parts.first.charset).to eq(charset)
end
it "raises error when 'from' isn't specified" do
expect { MissingFromMailer.new(configuration: configuration).deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
end
it "raises error when 'to' isn't specified" do
expect { MissingToMailer.new(configuration: configuration).deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
end
describe 'test delivery with hardcoded values' do
subject { WelcomeMailer.new(configuration: configuration).deliver({}) }
it 'sends the correct information' do
expect(subject.from).to eq(['noreply@sender.com'])
expect(subject.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
expect(subject.cc).to eq(['cc@recipient.com'])
expect(subject.bcc).to eq(['bcc@recipient.com'])
expect(subject.subject).to eq('Welcome')
end
it 'has the correct templates' do
expect(subject.html_part.to_s).to include(%(template))
expect(subject.text_part.to_s).to include(%(template))
end
it 'interprets the prepare statement' do
attachment = subject.attachments['invoice.pdf']
expect(attachment).to be_kind_of(Mail::Part)
expect(attachment).to be_attachment
expect(attachment).to_not be_inline
expect(attachment).to_not be_multipart
expect(attachment.filename).to eq('invoice.pdf')
expect(attachment.content_type).to match('application/pdf')
expect(attachment.content_type).to match('filename=invoice.pdf')
end
end
describe 'test delivery with procs' do
subject { ProcMailer.new(configuration: configuration).deliver(user: user) }
let(:user) { User.new('Name', 'student@deigirls.com') }
it 'sends the correct information' do
expect(subject.from).to eq(["hello-#{user.name.downcase}@example.com"])
expect(subject.to).to eq([user.email])
expect(subject.subject).to eq("[Hanami] Hello, #{user.name}")
end
end
describe 'test delivery with locals' do
subject { EventMailer.new(configuration: configuration) }
let(:count) { 100 }
it 'delivers the message' do
threads = []
mails = {}
count.times do |i|
threads << Thread.new do
user = OpenStruct.new(name: "Luca #{i}", email: "luca-#{i}@domain.test")
event = OpenStruct.new(id: i, title: "Event ##{i}")
mails[i] = subject.deliver(user: user, event: event)
end
end
threads.map(&:join)
expect(mails.count).to eq(count)
mails.each do |i, mail|
expect(mail.to).to eq(["luca-#{i}@domain.test"])
expect(mail.subject).to eq("Invitation: Event ##{i}")
expect(mail.attachments[0].filename).to eq("invitation-#{i}.ics")
end
end
end
describe 'multipart' do
it 'delivers all the parts by default' do
mail = WelcomeMailer.new(configuration: configuration).deliver({})
body = mail.body.encoded
expect(body).to include(%(<h1>Hello World!</h1>))
expect(body).to include(%(This is a txt template))
end
it 'can deliver only the text part' do
mail = WelcomeMailer.new(configuration: configuration).deliver(format: :txt)
body = mail.body.encoded
expect(body).to_not include(%(<h1>Hello World!</h1>))
expect(body).to include(%(This is a txt template))
end
it 'can deliver only the html part' do
mail = WelcomeMailer.new(configuration: configuration).deliver(format: :html)
body = mail.body.encoded
expect(body).to include(%(<h1>Hello World!</h1>))
expect(body).to_not include(%(This is a txt template))
end
end
describe 'custom delivery' do
before do
mailer.deliver({})
end
subject { options.fetch(:deliveries).first }
let(:mailer) { WelcomeMailer.new(configuration: configuration) }
let(:options) { { deliveries: [] } }
let(:configuration) do
configuration = Hanami::Mailer::Configuration.new do |config|
config.root = 'spec/support/fixtures'
config.delivery_method = MandrillDeliveryMethod, options
end
Hanami::Mailer.finalize(configuration)
end
it 'delivers the mail' do
expect(options.fetch(:deliveries).size).to be(1)
end
it 'sends the correct information' do
expect(subject.from).to eq(['noreply@sender.com'])
expect(subject.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
expect(subject.subject).to eq("Welcome")
end
it 'has the correct templates' do
expect(subject.html_part.to_s).to include(%(template))
expect(subject.text_part.to_s).to include(%(template))
end
it 'runs the before callback' do
expect(subject.attachments['invoice.pdf']).to_not be(nil)
end
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.configure do |config| RSpec.configure do |config|
config.expect_with :rspec do |expectations| config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.include_chain_clauses_in_custom_matcher_descriptions = true
@ -22,35 +24,9 @@ RSpec.configure do |config|
Kernel.srand config.seed Kernel.srand config.seed
end end
require 'ostruct'
require 'hanami/utils' require 'hanami/utils'
$LOAD_PATH.unshift 'lib' $LOAD_PATH.unshift 'lib'
require 'hanami/mailer' require 'hanami/mailer'
Hanami::Mailer.configure do
root Pathname.new __dir__ + '/support/fixtures/templates'
end
Hanami::Mailer.class_eval do
def self.reset!
self.configuration = configuration.duplicate
configuration.reset!
end
end
Hanami::Mailer::Dsl.class_eval do
def reset!
@templates = {}
end
end
Hanami::Utils.require!("spec/support") Hanami::Utils.require!("spec/support")
Hanami::Mailer.configure do
root __dir__ + '/support/fixtures'
delivery_method :test
prepare do
include DefaultSubject
end
end.load!

22
spec/support/context.rb Normal file
View File

@ -0,0 +1,22 @@
module RSpec
module Support
module Context
def self.included(base)
base.class_eval do
let(:configuration) do
configuration = Hanami::Mailer::Configuration.new do |config|
config.root = 'spec/support/fixtures'
config.delivery_method = :test
end
Hanami::Mailer.finalize(configuration)
end
end
end
end
end
end
RSpec.configure do |config|
config.include(RSpec::Support::Context)
end

View File

@ -1,42 +1,36 @@
class InvoiceMailer # frozen_string_literal: true
include Hanami::Mailer
class InvoiceMailer < Hanami::Mailer
template 'invoice' template 'invoice'
end end
class RenderMailer class RenderMailer < Hanami::Mailer
include Hanami::Mailer
end end
class TemplateEngineMailer class TemplateEngineMailer < Hanami::Mailer
include Hanami::Mailer
end end
class CharsetMailer class CharsetMailer < Hanami::Mailer
include Hanami::Mailer
from 'noreply@example.com' from 'noreply@example.com'
to 'user@example.com' to 'user@example.com'
subject 'こんにちは' subject 'こんにちは'
end end
class MissingFromMailer class MissingFromMailer < Hanami::Mailer
include Hanami::Mailer
template 'missing' template 'missing'
to 'recipient@example.com' to 'recipient@example.com'
subject 'Hello' subject 'Hello'
end end
class MissingToMailer class MissingToMailer < Hanami::Mailer
include Hanami::Mailer
template 'missing' template 'missing'
from 'sender@example.com' from 'sender@example.com'
subject 'Hello' subject 'Hello'
end end
class CcOnlyMailer class CcOnlyMailer < Hanami::Mailer
include Hanami::Mailer
template 'missing' template 'missing'
cc 'recipient@example.com' cc 'recipient@example.com'
@ -44,8 +38,7 @@ class CcOnlyMailer
subject 'Hello' subject 'Hello'
end end
class BccOnlyMailer class BccOnlyMailer < Hanami::Mailer
include Hanami::Mailer
template 'missing' template 'missing'
bcc 'recipient@example.com' bcc 'recipient@example.com'
@ -55,35 +48,20 @@ end
User = Struct.new(:name, :email) User = Struct.new(:name, :email)
class LazyMailer class LazyMailer < Hanami::Mailer
include Hanami::Mailer
end end
class MethodMailer class ProcMailer < Hanami::Mailer
include Hanami::Mailer from ->(locals) { "hello-#{locals.fetch(:user).name.downcase}@example.com" }
to ->(locals) { locals.fetch(:user).email }
subject ->(locals) { "[Hanami] #{locals.fetch(:greeting)}" }
from :sender before do |_, locals|
to :recipient locals[:greeting] = "Hello, #{locals.fetch(:user).name}"
subject :greeting
def greeting
"Hello, #{user.name}"
end
private
def sender
"hello-#{user.name.downcase}@example.com"
end
def recipient
user.email
end end
end end
class WelcomeMailer class WelcomeMailer < Hanami::Mailer
include Hanami::Mailer
from 'noreply@sender.com' from 'noreply@sender.com'
to ['noreply@recipient.com', 'owner@recipient.com'] to ['noreply@recipient.com', 'owner@recipient.com']
cc 'cc@recipient.com' cc 'cc@recipient.com'
@ -91,12 +69,34 @@ class WelcomeMailer
subject 'Welcome' subject 'Welcome'
before do |mail|
mail.attachments['invoice.pdf'] = "/path/to/invoice-#{invoice_code}.pdf"
end
def greeting def greeting
'Ahoy' 'Ahoy'
end end
def prepare def invoice_code
mail.attachments['invoice.pdf'] = '/path/to/invoice.pdf' "123"
end
end
class EventMailer < Hanami::Mailer
from 'events@domain.test'
to ->(locals) { locals.fetch(:user).email }
subject ->(locals) { "Invitation: #{locals.fetch(:event).title}" }
before do |mail, locals|
mail.attachments["invitation-#{locals.fetch(:event).id}.ics"] = generate_invitation_attachment(locals)
end
private
# Simulate on-the-fly creation of an attachment file.
# For speed purposes we're not gonna create the file, but only return a path.
def generate_invitation_attachment(locals)
"invitation-#{locals.fetch(:event).id}.ics"
end end
end end
@ -111,8 +111,14 @@ class MandrillDeliveryMethod
end end
module Users module Users
class Welcome class Welcome < Hanami::Mailer
include Hanami::Mailer end
end
module Web
module Mailers
class SignupMailer < Hanami::Mailer
end
end end
end end

View File

@ -0,0 +1,2 @@
Fixture used by
spec/unit/hanami/mailer/templates_finder_spec.rb

View File

@ -0,0 +1,2 @@
Fixture used by
spec/unit/hanami/mailer/templates_finder_spec.rb

View File

@ -0,0 +1,2 @@
Fixture used by
spec/unit/hanami/mailer/templates_finder_spec.rb

View File

@ -1,21 +1,21 @@
RSpec.describe Hanami::Mailer::Configuration do # frozen_string_literal: true
before do
@configuration = Hanami::Mailer::Configuration.new
end
describe '#root' do RSpec.describe Hanami::Mailer::Configuration do
subject { described_class.new }
describe '#root=' do
describe 'when a value is given' do describe 'when a value is given' do
describe 'and it is a string' do describe 'and it is a string' do
it 'sets it as a Pathname' do it 'sets it as a Pathname' do
@configuration.root 'spec' subject.root = 'spec'
expect(@configuration.root).to eq(Pathname.new('spec').realpath) expect(subject.root).to eq(Pathname.new('spec').realpath)
end end
end end
describe 'and it is a pathname' do describe 'and it is a pathname' do
it 'sets it' do it 'sets it' do
@configuration.root Pathname.new('spec') subject.root = Pathname.new('spec')
expect(@configuration.root).to eq(Pathname.new('spec').realpath) expect(subject.root).to eq(Pathname.new('spec').realpath)
end end
end end
@ -33,15 +33,15 @@ RSpec.describe Hanami::Mailer::Configuration do
end end
it 'sets the converted value' do it 'sets the converted value' do
@configuration.root RootPath.new('spec') subject.root = RootPath.new('spec')
expect(@configuration.root).to eq(Pathname.new('spec').realpath) expect(subject.root).to eq(Pathname.new('spec').realpath)
end end
end end
describe 'and it is an unexisting path' do describe 'and it is an unexisting path' do
it 'raises an error' do it 'raises an error' do
expect do expect do
@configuration.root '/path/to/unknown' subject.root = '/path/to/unknown'
end.to raise_error(Errno::ENOENT) end.to raise_error(Errno::ENOENT)
end end
end end
@ -49,146 +49,69 @@ RSpec.describe Hanami::Mailer::Configuration do
describe 'when a value is not given' do describe 'when a value is not given' do
it 'defaults to the current path' do it 'defaults to the current path' do
expect(@configuration.root).to eq(Pathname.new('.').realpath) expect(subject.root).to eq(Pathname.new('.').realpath)
end end
end end
end end
describe '#mailers' do
it 'defaults to an empty set' do
expect(@configuration.mailers).to be_empty
end
it 'allows to add mailers' do
@configuration.add_mailer(InvoiceMailer)
expect(@configuration.mailers).to include(InvoiceMailer)
end
it 'eliminates duplications' do
@configuration.add_mailer(RenderMailer)
@configuration.add_mailer(RenderMailer)
expect(@configuration.mailers.size).to eq(1)
end
end
describe '#prepare' do
before do
module FooRendering
def render
'foo'
end
end
class PrepareMailer
end
end
after do
Object.__send__(:remove_const, :FooRendering)
Object.__send__(:remove_const, :PrepareMailer)
end
it 'raises error in case of missing block' do
expect { @configuration.prepare }.to raise_error(ArgumentError, 'Please provide a block')
end
end
# describe '#reset!' do
# before do
# @configuration.root 'test'
# @configuration.add_mailer(InvoiceMailer)
# @configuration.reset!
# end
# it 'resets root' do
# root = Pathname.new('.').realpath
# @configuration.root.must_equal root
# @configuration.mailers.must_be_empty
# end
# it "doesn't reset namespace" do
# @configuration.namespace(InvoiceMailer)
# @configuration.reset!
# @configuration.namespace.must_equal(InvoiceMailer)
# end
# end
describe '#load!' do
before do
@configuration.root 'spec'
@configuration.load!
end
it 'loads root' do
root = Pathname.new('spec').realpath
expect(@configuration.root).to eq(root)
end
end
describe '#delivery_method' do describe '#delivery_method' do
describe 'when not previously set' do describe 'when not previously set' do
before do
@configuration.reset!
end
it 'defaults to SMTP' do it 'defaults to SMTP' do
expect(@configuration.delivery_method).to eq([:smtp, {}]) expect(subject.delivery_method).to eq(:smtp)
end end
end end
describe 'set with a symbol' do describe 'set with a symbol' do
before do before do
@configuration.delivery_method :exim, location: '/path/to/exim' subject.delivery_method = :exim, { location: '/path/to/exim' }
end end
it 'saves the delivery method in the configuration' do it 'saves the delivery method in the configuration' do
expect(@configuration.delivery_method).to eq([:exim, { location: '/path/to/exim' }]) expect(subject.delivery_method).to eq([:exim, { location: '/path/to/exim' }])
end end
end end
describe 'set with a class' do describe 'set with a class' do
before do before do
@configuration.delivery_method MandrillDeliveryMethod, subject.delivery_method = MandrillDeliveryMethod,
username: 'mandrill-username', password: 'mandrill-api-key' { username: 'mandrill-username', password: 'mandrill-api-key' }
end end
it 'saves the delivery method in the configuration' do it 'saves the delivery method in the configuration' do
expect(@configuration.delivery_method).to eq([MandrillDeliveryMethod, username: 'mandrill-username', password: 'mandrill-api-key']) expect(subject.delivery_method).to eq([MandrillDeliveryMethod, username: 'mandrill-username', password: 'mandrill-api-key'])
end end
end end
end end
describe '#default_charset' do describe '#default_charset' do
describe 'when not previously set' do describe 'when not previously set' do
before do
@configuration.reset!
end
it 'defaults to UTF-8' do it 'defaults to UTF-8' do
expect(@configuration.default_charset).to eq('UTF-8') expect(subject.default_charset).to eq('UTF-8')
end end
end end
describe 'when set' do describe 'when set' do
before do before do
@configuration.default_charset 'iso-8859-1' subject.default_charset = 'iso-8859-1'
end end
it 'saves the delivery method in the configuration' do it 'saves the delivery method in the configuration' do
expect(@configuration.default_charset).to eq('iso-8859-1') expect(subject.default_charset).to eq('iso-8859-1')
end end
end end
end end
describe '#prepare' do describe "#freeze" do
it 'injects code in each mailer' before do
# it 'injects code in each mailer' do subject.freeze
# InvoiceMailer.subject.must_equal 'default subject' end
# end
it "is frozen" do
expect(subject).to be_frozen
end
it "raises error if trying to add a mailer" do
expect { subject.add_mailer(WelcomeMailer) }.to raise_error(RuntimeError)
end
end end
end end

View File

@ -1,184 +0,0 @@
RSpec.describe Hanami::Mailer do
describe '.deliver' do
before do
Hanami::Mailer.deliveries.clear
end
it 'can deliver with specified charset' do
CharsetMailer.deliver(charset: charset = 'iso-2022-jp')
mail = Hanami::Mailer.deliveries.first
expect(mail.charset).to eq(charset)
expect(mail.parts.first.charset).to eq(charset)
end
it "raises error when 'from' isn't specified" do
expect { MissingFromMailer.deliver }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
end
it "raises error when 'to' isn't specified" do
expect { MissingToMailer.deliver }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
end
it "doesn't raise error when 'cc' is specified but 'to' isn't" do
CcOnlyMailer.deliver
end
it "doesn't raise error when 'bcc' is specified but 'to' isn't" do
BccOnlyMailer.deliver
end
it "lets other errors to bubble up" do
mailer = CharsetMailer.new({})
mail = Class.new do
def deliver
raise ArgumentError, "ouch"
end
end.new
expect(mailer).to receive(:mail).and_return(mail)
expect { mailer.deliver }.to raise_error(ArgumentError, "ouch")
end
describe 'test delivery with hardcoded values' do
before do
WelcomeMailer.deliver
@mail = Hanami::Mailer.deliveries.first
end
after do
Hanami::Mailer.deliveries.clear
end
it 'delivers the mail' do
expect(Hanami::Mailer.deliveries.length).to eq(1)
end
it 'sends the correct information' do
expect(@mail.from).to eq(['noreply@sender.com'])
expect(@mail.to).to eq(['noreply@recipient.com', 'owner@recipient.com'])
expect(@mail.cc).to eq(['cc@recipient.com'])
expect(@mail.bcc).to eq(['bcc@recipient.com'])
expect(@mail.subject).to eq('Welcome')
end
it 'has the correct templates' do
expect(@mail.html_part.to_s).to include(%(template))
expect(@mail.text_part.to_s).to include(%(template))
end
it 'interprets the prepare statement' do
attachment = @mail.attachments['invoice.pdf']
expect(attachment).to be_kind_of(Mail::Part)
expect(attachment).to be_attachment
expect(attachment).to_not be_inline
expect(attachment).to_not be_multipart
expect(attachment.filename).to eq('invoice.pdf')
expect(attachment.content_type).to match('application/pdf')
expect(attachment.content_type).to match('filename=invoice.pdf')
end
end
describe 'test delivery with methods' do
before do
@user = User.new('Name', 'student@deigirls.com')
MethodMailer.deliver(user: @user)
@mail = Hanami::Mailer.deliveries.first
end
after do
Hanami::Mailer.deliveries.clear
end
it 'delivers the mail' do
expect(Hanami::Mailer.deliveries.length).to eq(1)
end
it 'sends the correct information' do
expect(@mail.from).to eq(["hello-#{@user.name.downcase}@example.com"])
expect(@mail.to).to eq([@user.email])
expect(@mail.subject).to eq("Hello, #{@user.name}")
end
end
describe 'multipart' do
after do
Hanami::Mailer.deliveries.clear
end
it 'delivers all the parts by default' do
WelcomeMailer.deliver
mail = Hanami::Mailer.deliveries.first
body = mail.body.encoded
expect(body).to include(%(<h1>Hello World!</h1>))
expect(body).to include(%(This is a txt template))
end
it 'can deliver only the text part' do
WelcomeMailer.deliver(format: :txt)
mail = Hanami::Mailer.deliveries.first
body = mail.body.encoded
expect(body).to_not include(%(<h1>Hello World!</h1>))
expect(body).to include(%(This is a txt template))
end
it 'can deliver only the html part' do
WelcomeMailer.deliver(format: :html)
mail = Hanami::Mailer.deliveries.first
body = mail.body.encoded
expect(body).to include(%(<h1>Hello World!</h1>))
expect(body).to_not include(%(This is a txt template))
end
end
describe 'custom delivery' do
before do
@options = options = { deliveries: [] }
# Hanami::Mailer.reset!
# Hanami::Mailer.configure do
# delivery_method MandrillDeliveryMethod, options
# end.load!
WelcomeMailer.deliver
@mail = options.fetch(:deliveries).first
end
it 'delivers the mail'
# it 'delivers the mail' do
# @options.fetch(:deliveries).length.must_equal 1
# end
# it 'sends the correct information' do
# @mail.from.must_equal ['noreply@sender.com']
# @mail.to.must_equal ['noreply@recipient.com']
# @mail.subject.must_equal "Welcome"
# end
# it 'has the correct templates' do
# @mail.html_part.to_s.must_include %(template)
# @mail.text_part.to_s.must_include %(template)
# end
# it 'interprets the prepare statement' do
# @mail.attachments['invoice.pdf'].wont_be_nil
# end
# it 'adds the attachment to the mail object' do
# @mail.attachments['render_mailer.html.erb'].wont_be_nil
# end
end
end
end

View File

@ -1,47 +1,123 @@
RSpec.describe Hanami::Mailer do # frozen_string_literal: true
describe '.template' do
describe 'when no value is set' do
it 'returns the convention name' do
expect(RenderMailer.template).to eq('render_mailer')
end
it 'returns correct namespaced value' do RSpec.describe Hanami::Mailer::Dsl do
expect(Users::Welcome.template).to eq('users/welcome') let(:mailer) { Class.new { extend Hanami::Mailer::Dsl } }
end
describe '.from' do
it 'returns the default value' do
expect(mailer.from).to be(nil)
end end
describe 'when a value is set' do it 'sets the value' do
it 'returns that name' do sender = 'sender@hanami.test'
expect(InvoiceMailer.template).to eq('invoice') mailer.from sender
end
expect(mailer.from).to eq(sender)
end end
end end
describe '.templates' do describe '.to' do
describe 'when no value is set' do it 'returns the default value' do
it 'returns a set of templates' do expect(mailer.to).to be(nil)
template_formats = LazyMailer.templates.keys
expect(template_formats).to eq(%i[html txt])
end
it 'returns only the template for the given format' do
template = LazyMailer.templates(:txt)
expect(template).to be_kind_of(Hanami::Mailer::Template)
expect(template.file).to match(%r{spec/support/fixtures/templates/lazy_mailer.txt.erb\z})
end
end end
describe 'when a value is set' do it 'sets a single value' do
it 'returns a set of templates' do recipient = 'recipient@hanami.test'
template_formats = InvoiceMailer.templates.keys mailer.to recipient
expect(template_formats).to eq([:html])
end
it 'returns only the template for the given format' do expect(mailer.to).to eq(recipient)
template = InvoiceMailer.templates(:html) end
expect(template).to be_kind_of(Hanami::Mailer::Template)
expect(template.file).to match(%r{spec/support/fixtures/templates/invoice.html.erb\z}) it 'sets an array of values' do
end recipients = ['recipient@hanami.test']
mailer.to recipients
expect(mailer.to).to eq(recipients)
end
end
describe '.cc' do
it 'returns the default value' do
expect(mailer.cc).to be(nil)
end
it 'sets a single value' do
recipient = 'cc@hanami.test'
mailer.cc recipient
expect(mailer.cc).to eq(recipient)
end
it 'sets an array of values' do
recipients = ['cc@hanami.test']
mailer.cc recipients
expect(mailer.cc).to eq(recipients)
end
end
describe '.bcc' do
it 'returns the default value' do
expect(mailer.bcc).to be(nil)
end
it 'sets a single value' do
recipient = 'bcc@hanami.test'
mailer.bcc recipient
expect(mailer.bcc).to eq(recipient)
end
it 'sets an array of values' do
recipients = ['bcc@hanami.test']
mailer.bcc recipients
expect(mailer.bcc).to eq(recipients)
end
end
describe '.subject' do
it 'returns the default value' do
expect(mailer.subject).to be(nil)
end
it 'sets a value' do
mail_subject = 'Hello'
mailer.subject mail_subject
expect(mailer.subject).to eq(mail_subject)
end
end
describe '.template' do
it 'sets a value' do
mailer.template 'file'
end
end
describe '.template_name' do
it 'returns the default value' do
expect(mailer.template_name).to be(nil)
end
it 'returns value, if set' do
template = 'file'
mailer.template template
expect(mailer.template_name).to eq(template)
end
end
describe '.before' do
it 'returns the default value' do
expect(mailer.before).to be_kind_of(Proc)
end
it 'sets a value' do
blk = ->(*) {}
mailer.before(&blk)
expect(mailer.before).to eq(blk)
end end
end end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::Error do
it "inherits from StandardError" do
expect(described_class.ancestors).to include(StandardError)
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::Finalizer do
let(:configuration) do
Hanami::Mailer::Configuration.new do |config|
config.root = 'spec/support/fixtures'
end
end
let(:mailers) { [double('mailer', template_name: "invoice")] }
describe '.finalize' do
it 'eager autoloads modules from mail gem' do
expect(Mail).to receive(:eager_autoload!)
described_class.finalize(mailers, configuration)
end
it "adds the mailer to the configuration" do
expect(configuration).to receive(:add_mailer).with(mailers.first)
described_class.finalize(mailers, configuration)
end
it "returns frozen configuration" do
actual = described_class.finalize(mailers, configuration)
expect(actual).to be_frozen
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::MissingDeliveryDataError do
it "inherits from Hanami::Error" do
expect(described_class.ancestors).to include(Hanami::Mailer::Error)
end
it "has a custom error message" do
expect { raise described_class }.to raise_error(described_class, "Missing delivery data, please check 'from', or 'to'")
end
end

View File

@ -1,44 +0,0 @@
RSpec.describe Hanami::Mailer do
describe '#render' do
describe 'when template is explicitly declared' do
let(:mailer) { InvoiceMailer.new }
it 'renders the given template' do
expect(mailer.render(:html)).to include(%(<h1>Invoice template</h1>))
end
end
describe 'when template is implicitly declared' do
let(:mailer) { LazyMailer.new }
it 'looks for template with same name with inflected classname and render it' do
expect(mailer.render(:html)).to include(%(Hello World))
expect(mailer.render(:txt)).to include(%(This is a txt template))
end
end
describe 'when mailer defines context' do
let(:mailer) { WelcomeMailer.new }
it 'renders template with defined context' do
expect(mailer.render(:txt)).to include(%(Ahoy))
end
end
describe 'when locals are parsed in' do
let(:mailer) { RenderMailer.new(user: User.new('Luca')) }
it 'renders template with parsed locals' do
expect(mailer.render(:html)).to include(%(Luca))
end
end
describe 'with HAML template engine' do
let(:mailer) { TemplateEngineMailer.new(user: User.new('Luca')) }
it 'renders template with parsed locals' do
expect(mailer.render(:html)).to include(%(<h1>\nLuca\n</h1>\n))
end
end
end
end

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::TemplateName do
describe ".call" do
context "with top level namespace" do
let(:namespace) { Object }
it "returns an instance of ::String" do
template_name = described_class.call("template", namespace)
expect(template_name).to be_kind_of(String)
end
it "returns name from plain name" do
template_name = described_class.call("template", namespace)
expect(template_name).to eq("template")
end
it "returns name from camel case name" do
template_name = described_class.call("ATemplate", namespace)
expect(template_name).to eq("a_template")
end
it "returns name from snake case name" do
template_name = described_class.call("a_template", namespace)
expect(template_name).to eq("a_template")
end
it "returns name from modulized name" do
template_name = described_class.call("Mailers::WelcomeMailer", namespace)
expect(template_name).to eq("mailers/welcome_mailer")
end
it "returns name from class" do
template_name = described_class.call(InvoiceMailer.name, namespace)
expect(template_name).to eq("invoice_mailer")
end
it "returns name from modulized class" do
template_name = described_class.call(Users::Welcome.name, namespace)
expect(template_name).to eq("users/welcome")
end
it "returns blank string from blank name" do
template_name = described_class.call("", namespace)
expect(template_name).to eq("")
end
it "raises error with nil name" do
expect { described_class.call(nil, namespace) }.to raise_error(NoMethodError)
end
end
context "with application namespace" do
let(:namespace) { Web::Mailers }
it "returns name from class name" do
template_name = described_class.call("SignupMailer", namespace)
expect(template_name).to eq("signup_mailer")
end
it "returns name from modulized class name" do
template_name = described_class.call(Web::Mailers::SignupMailer.name, namespace)
expect(template_name).to eq("signup_mailer")
end
end
end
end

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::Template do
subject { described_class.new(file) }
let(:file) { "spec/support/fixtures/templates/welcome_mailer.txt.erb" }
describe "#initialize" do
context "with existing file" do
it "instantiates template" do
expect(subject).to be_kind_of(described_class)
end
it "initialize frozen instance" do
expect(subject).to be_frozen
end
end
context "with missing template engine" do
it "returns error" do
expect { described_class.new("Gemfile") }.to raise_error(RuntimeError, "No template engine registered for Gemfile")
end
end
context "with unexisting file" do
it "returns error" do
expect { described_class.new("foo.erb") }.to raise_error(Errno::ENOENT)
end
end
end
describe "#render" do
it "renders template" do
scope = Object.new
actual = subject.render(scope, greeting: "Hello")
expect(actual).to eq("This is a txt template\nHello")
end
it "renders with unfrozen object" do
scope = Object.new
expect(scope).to receive(:dup)
subject.render(scope, greeting: "Hello")
end
end
end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::TemplatesFinder do
subject { described_class.new(root) }
# NOTE: please do not change this name, because `#find` specs are relying on
# template fixtures. See all the fixtures under: spec/support/fixtures/templates
let(:template_name) { "welcome_mailer" }
let(:root) { configuration.root }
describe "#initialize" do
context "with valid root" do
it "instantiates a new finder instance" do
expect(subject).to be_kind_of(described_class)
end
it "returns a frozen object" do
expect(subject).to be_frozen
end
end
context "with unexisting root" do
let(:root) { "path/to/unexisting" }
it "raises error" do
expect { subject }.to raise_error(Errno::ENOENT)
end
end
context "with nil root" do
let(:root) { nil }
it "raises error" do
expect { subject }.to raise_error(TypeError)
end
end
end
describe "#find" do
context "with valid template name" do
it "returns templates" do
actual = subject.find(template_name)
# It excludes all the files that aren't matching the convention:
#
# `<template_name>.<format>.<template_engine>`
#
# Under `spec/support/fixtures/templates` we have the following files:
#
# * welcome_mailer
# * welcome_mailer.erb
# * welcome_mailer.html
# * welcome_mailer.html.erb
# * welcome_mailer.txt.erb
#
# Only the last two are matching the pattern, here's why we have only
# two templates loaded.
expect(actual.keys).to eq(%i[html txt])
actual.each_value do |template|
expect(template).to be_kind_of(Hanami::Mailer::Template)
expect(template.instance_variable_get(:@_template).__send__(:file)).to match(%r{spec/support/fixtures/templates/welcome_mailer.(html|txt).erb})
end
end
end
context "with missing template" do
let(:template_name) { "missing_template" }
it "doesn't return templates" do
actual = subject.find(template_name)
expect(actual).to be_empty
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer::UnknownMailerError do
it "inherits from Hanami::Error" do
expect(described_class.ancestors).to include(Hanami::Mailer::Error)
end
it "has a custom error message" do
mailer = InvoiceMailer
expect { raise described_class.new(mailer) }.to raise_error(described_class, "Unknown mailer: #{mailer}. Please finalize the configuration before to use it.")
end
it "has explicit handling for nil" do
mailer = nil
expect { raise described_class.new(mailer) }.to raise_error(described_class, "Unknown mailer: #{mailer.inspect}. Please finalize the configuration before to use it.")
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
RSpec.describe "Hanami::Mailer::VERSION" do RSpec.describe "Hanami::Mailer::VERSION" do
it "returns current version" do it "returns current version" do
expect(Hanami::Mailer::VERSION).to eq("1.1.0") expect(Hanami::Mailer::VERSION).to eq("1.1.0")

View File

@ -0,0 +1,168 @@
# frozen_string_literal: true
RSpec.describe Hanami::Mailer do
context "constants" do
it "marks them as private" do
expect { described_class::CONTENT_TYPES }.to raise_error(NameError)
end
end
context ".finalize" do
let(:configuration) { Hanami::Mailer::Configuration.new }
it "finalizes the given configuration" do
actual = described_class.finalize(configuration)
expect(actual).to be_frozen
end
end
context "#initialize" do
let(:configuration) { Hanami::Mailer::Configuration.new }
it "builds an frozen instance" do
mailer = described_class.new(configuration: configuration)
expect(mailer).to be_frozen
end
it "prevents memoization mutations" do
mailer = Class.new(described_class) do
def self.name
"memoization_attempt"
end
def foo
@foo ||= "foo"
end
end.new(configuration: configuration)
expect { mailer.foo }.to raise_error(RuntimeError)
end
it "prevents accidental configuration removal" do
mailer = Class.new(described_class) do
def self.name
"configuration_removal"
end
def foo
@configuration = nil
end
end.new(configuration: configuration)
expect { mailer.foo }.to raise_error(RuntimeError)
end
end
context "#deliver" do
context "when mailer has from/to defined with DSL" do
let(:mailer) { CharsetMailer.new(configuration: configuration) }
it "delivers email with valid locals" do
mail = mailer.deliver({})
expect(mail).to be_kind_of(Mail::Message)
end
it "is aliased as #call" do
mail = mailer.call({})
expect(mail).to be_kind_of(Mail::Message)
end
it "raises error when locals are nil" do
expect { mailer.deliver(nil) }.to raise_error(NoMethodError)
end
end
context "when from/to are missing" do
let(:mailer) { InvoiceMailer.new(configuration: configuration) }
it "raises error" do
expect { mailer.deliver({}) }.to raise_error(Hanami::Mailer::MissingDeliveryDataError)
end
end
context "when using #{described_class} directly" do
let(:mailer) do
described_class.new(configuration: configuration)
end
it "raises error" do
expect { mailer.deliver({}) }.to raise_error(NoMethodError)
end
end
context "with non-finalized configuration" do
let(:configuration) { Hanami::Mailer::Configuration.new }
let(:mailer) do
Class.new(described_class) do
def self.name
"anonymous_mailer"
end
end.new(configuration: configuration)
end
it "raises error" do
expect { mailer.deliver({}) }.to raise_error(Hanami::Mailer::UnknownMailerError)
end
end
context "locals" do
let(:mailer) { EventMailer.new(configuration: configuration) }
let(:user) { OpenStruct.new(name: "Luca", email: "luca@domain.test") }
let(:event) { OpenStruct.new(id: 23, title: "Event #23") }
it "uses locals during the delivery process" do
mail = mailer.deliver(user: user, event: event)
expect(mail.to).to eq(["luca@domain.test"])
expect(mail.subject).to eq("Invitation: Event #23")
expect(mail.attachments[0].filename).to eq("invitation-23.ics")
end
end
end
describe '#render' do
describe 'when template is explicitly declared' do
let(:mailer) { InvoiceMailer.new(configuration: configuration) }
it 'renders the given template' do
expect(mailer.render(:html, {})).to include(%(<h1>Invoice template</h1>))
end
end
describe 'when template is implicitly declared' do
let(:mailer) { LazyMailer.new(configuration: configuration) }
it 'looks for template with same name with inflected classname and render it' do
expect(mailer.render(:html, {})).to include(%(Hello World))
expect(mailer.render(:txt, {})).to include(%(This is a txt template))
end
end
describe 'when mailer defines context' do
let(:mailer) { WelcomeMailer.new(configuration: configuration) }
it 'renders template with defined context' do
expect(mailer.render(:txt, {})).to include(%(Ahoy))
end
end
describe 'when locals are parsed in' do
let(:mailer) { RenderMailer.new(configuration: configuration) }
let(:locals) { { user: User.new('Luca') } }
it 'renders template with parsed locals' do
expect(mailer.render(:html, locals)).to include(locals.fetch(:user).name)
end
end
describe 'with HAML template engine' do
let(:mailer) { TemplateEngineMailer.new(configuration: configuration) }
let(:locals) { { user: User.new('MG') } }
it 'renders template with parsed locals' do
expect(mailer.render(:html, locals)).to include(%(<h1>\n#{locals.fetch(:user).name}\n</h1>\n))
end
end
end
end