hanami-mailer 1.3.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class Mailer
5
+ # Integration module for Hanami::View support.
6
+ #
7
+ # This module is included when Hanami::View is available, providing automatic view building and
8
+ # settings inheritance.
9
+ #
10
+ # @api private
11
+ module ViewIntegration
12
+ def self.included(base)
13
+ base.class_eval do
14
+ # Prepend the initializer module to wrap initialization
15
+ prepend PrependedMethods
16
+
17
+ # Add class-level methods, including the lazily-built, memoized default view
18
+ extend ClassMethods
19
+
20
+ # Whether to automatically build views from exposures
21
+ # Set to false to disable automatic view integration behavior
22
+ setting :integrate_view, default: true
23
+
24
+ # The base class used when building the mailer's view. Defaults to Hanami::View, but
25
+ # may be set to an already-configured view class (such as a view class within a Hanami
26
+ # app), in which case the built view inherits that class's configuration — context,
27
+ # parts, scopes, paths, helpers and so on.
28
+ setting :view_class, default: Hanami::View
29
+
30
+ # Copy all settings from Hanami::View to support default view integration.
31
+ # This allows mailers to configure view-related settings (like layouts_dir,
32
+ # default_format, inflector, etc.) without having to manually redefine them.
33
+ existing_settings = config._settings.keys.to_set
34
+ Hanami::View.config._settings.each do |setting_def|
35
+ next if existing_settings.include?(setting_def.name)
36
+
37
+ setting(
38
+ setting_def.name,
39
+ default: setting_def.default,
40
+ constructor: setting_def.constructor,
41
+ **setting_def.options
42
+ )
43
+ end
44
+ end
45
+ end
46
+
47
+ # Class-level methods added to mailer classes when Hanami::View is available.
48
+ #
49
+ # @api private
50
+ module ClassMethods
51
+ # The auto-built default view for this mailer class.
52
+ #
53
+ # Built lazily on first access (the first render) and memoized, since it depends only on
54
+ # class-level state (config, exposures, class name) and is identical for every instance.
55
+ # Returns nil when view integration is disabled or no usable template paths are configured.
56
+ #
57
+ # Note: because the view is memoized on first render, later changes to view-related
58
+ # config or exposures are not reflected. Mailer classes are configured once at definition
59
+ # time, so this is intentional.
60
+ #
61
+ # @api private
62
+ def default_view
63
+ return @default_view if defined?(@default_view)
64
+
65
+ @default_view = config.integrate_view ? DefaultViewBuilder.call(self) : nil
66
+ end
67
+
68
+ # Defines an exposure that will be decorated with a matching view part.
69
+ #
70
+ # This is a shorthand for `expose(..., decorate: true)`.
71
+ #
72
+ # @see Mailer.expose
73
+ def decorate(*names, **options, &block)
74
+ expose(*names, **options, decorate: true, &block)
75
+ end
76
+ end
77
+
78
+ # Internal module for prepending view and render behavior.
79
+ # Wraps the base class to provide automatic view building and
80
+ # per-format template error handling.
81
+ #
82
+ # @api private
83
+ module PrependedMethods
84
+ # The view used for rendering: a per-instance override passed to the constructor, falling
85
+ # back to the mailer class's lazily-built, memoized default view.
86
+ def view
87
+ @view || self.class.default_view
88
+ end
89
+
90
+ # Renders HTML and text bodies, handling missing templates per format.
91
+ def render(input, format: nil)
92
+ html, html_error = try_render(:html, input) unless format == :text
93
+ text, text_error = try_render(:text, input) unless format == :html
94
+
95
+ # Tolerate one missing template if attempting to render both. Otherwise, consider any
96
+ # error as fatal.
97
+ raise html_error if html_error && (format || text_error)
98
+ raise text_error if text_error && format
99
+
100
+ [html, text]
101
+ end
102
+
103
+ def try_render(format, input)
104
+ [render_view(format, input), nil]
105
+ rescue Hanami::View::TemplateNotFoundError => exception
106
+ [nil, exception]
107
+ end
108
+ end
109
+
110
+ # Builder class for constructing default views.
111
+ # Keeps view building logic separate from mailer instances.
112
+ #
113
+ # @api private
114
+ class DefaultViewBuilder
115
+ class << self
116
+ # Builds a default view from exposures if Hanami::View is available.
117
+ def call(mailer_class)
118
+ view_class = mailer_class.config.view_class || Hanami::View
119
+
120
+ # A view needs paths to find its templates. These may be configured on the mailer, or
121
+ # inherited from an already-configured `view_class` (e.g. within a Hanami app).
122
+ paths = mailer_class.config.paths
123
+ if (paths.nil? || paths.empty?) && view_class.respond_to?(:config)
124
+ paths = view_class.config.paths
125
+ end
126
+ return nil if paths.nil? || paths.empty?
127
+
128
+ template = mailer_class.config.template
129
+ template ||= inferred_template(mailer_class)
130
+
131
+ build_view_class(
132
+ view_class: view_class,
133
+ template: template,
134
+ exposures: mailer_class.exposures,
135
+ config: mailer_class.config
136
+ )
137
+ end
138
+
139
+ private
140
+
141
+ # Infers template path from class name.
142
+ #
143
+ # @example
144
+ # Mailers::WelcomeMailer -> "mailers/welcome_mailer"
145
+ def inferred_template(mailer_class)
146
+ return nil unless mailer_class.name
147
+
148
+ mailer_class.name
149
+ .gsub("::", "/")
150
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
151
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
152
+ .downcase
153
+ end
154
+
155
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
156
+
157
+ # Builds a Hanami::View instance from the mailer's configuration.
158
+ #
159
+ # The view is a subclass of the mailer's configured `view_class`, so it inherits that
160
+ # class's configuration. Only view settings the mailer has *explicitly* configured are
161
+ # applied as overrides, leaving an already-configured base class (such as a Hanami app's
162
+ # view) to provide its context, parts, scopes and helpers by inheritance. A standalone
163
+ # mailer (whose `view_class` is the unconfigured `Hanami::View`) still drives its own
164
+ # view via these overrides.
165
+ def build_view_class(view_class:, template:, exposures:, config:)
166
+ view_template = template
167
+ view_exposures = exposures
168
+ mailer_config = config
169
+
170
+ built = Class.new(view_class) do
171
+ Hanami::View.config._settings.each do |setting_def|
172
+ name = setting_def.name
173
+
174
+ # `template` and `layout` are handled explicitly below.
175
+ next if name == :template || name == :layout
176
+ next unless mailer_config.respond_to?(name)
177
+ next unless mailer_config.configured?(name)
178
+
179
+ self.config.public_send(:"#{name}=", mailer_config.public_send(name))
180
+ end
181
+
182
+ # Mailers do not use a layout by default, but one may be configured.
183
+ self.config.layout = mailer_config.configured?(:layout) ? mailer_config.layout : false
184
+
185
+ self.config.template = view_template if view_template
186
+
187
+ # Exposures are evaluated once, by the mailer, which passes their values to the view.
188
+ # The view only needs to pass each value through to the template (decorating it as
189
+ # required), so the procs are dropped here. Private exposures are internal to the
190
+ # mailer's evaluation and are not passed to the view at all.
191
+ view_exposures.each do |name, exposure|
192
+ next if exposure.private?
193
+
194
+ expose(name, **exposure.options)
195
+ end
196
+ end
197
+
198
+ built.new
199
+ end
200
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end