padrino-mailer 0.18.2 → 0.18.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac2d83b55958d57fb6cced02c2ec08e3613982594069bc9b08036bd95f02e63b
4
- data.tar.gz: 0500d718cf0f13ca82fd90097cec7548eb4e824e0e72678bb1cffff87f947955
3
+ metadata.gz: b11b32ce2d59a91d317eaa40ad82491aad78b7717d269846ff009ff8b1faec13
4
+ data.tar.gz: 5bcc8d002ca343172c0706f834ecfc8d7e559d516c1a8fdab94c77d5c82519fa
5
5
  SHA512:
6
- metadata.gz: ae82b89755b7e13c0c66b629ff060f3e24ea509fba47b0e9259ec8a637230021f109bdb8edb70b56f31c2cc8b54c09ad4185240f5f8cbcf0e3cbcf93d72bf713
7
- data.tar.gz: 98d1b6fd13eb78d43fac67b5e1372ed01fe4bb6d5312b5fd73bdb0df0cf1ebaffd89ab44d22ab03fcdd9b3c1a4f25f685f0b1aacc618e5ca16c9fab13e8210aa
6
+ metadata.gz: e2999d5468663f308a0f3ec49ba3f53e793a8bd9f79e3dd741465c3ceedcad4cf94e0ef8906eba3d78a18c81387b7e5d70648eb8a396e08bc3d609e2fe6b782c
7
+ data.tar.gz: '01877f03e82419e0adc3f50e2c9ff719db6ea4d4f774714faae403a09f2f6a2cf2955dece2d2f415c9bdeceb3e32de21ef42e894cf042ef2f0f55dd50ed18ba1'
@@ -9,16 +9,57 @@ module Mail # @private
9
9
  attr_reader :template_cache
10
10
  attr_accessor :mailer_name, :message_name
11
11
 
12
+ # The template cache of a message whose app doesn't reload templates. Templates given by name
13
+ # (a file or a named template) are compiled once for the class and shared by its messages;
14
+ # templates given as a string stay with the message, so they don't pile up.
15
+ class SharedTemplateCache < Sinatra::TemplateCache # @private
16
+ def initialize(owner)
17
+ super()
18
+ @owner = owner
19
+ end
20
+
21
+ def fetch(engine, data, *rest, &block)
22
+ return super unless data.is_a?(Symbol)
23
+
24
+ @shared ||= @owner.shared_template_cache
25
+ @shared.fetch(engine, data, *rest, &block)
26
+ end
27
+
28
+ # Message#to_yaml dumps every instance variable; keep the class's templates and lock out of it.
29
+ def encode_with(coder); end
30
+ end
31
+
32
+ # The cache a class's messages share. Messages are often delivered from several threads, and
33
+ # on JRuby writing to a Hash from two threads at once can raise, so it takes a lock. It compiles
34
+ # outside the lock, so two threads may compile one template; the first one stored is kept.
35
+ class LockedTemplateCache # @private
36
+ def initialize
37
+ @cache = {}
38
+ @lock = Mutex.new
39
+ end
40
+
41
+ def fetch(*key)
42
+ @lock.synchronize { return @cache[key] if @cache.key?(key) }
43
+ template = yield
44
+ @lock.synchronize { @cache.fetch(key) { @cache[key] = template } }
45
+ end
46
+ end
47
+
48
+ SHARED_TEMPLATE_CACHE_LOCK = Mutex.new # @private
49
+
50
+ def self.shared_template_cache # @private
51
+ SHARED_TEMPLATE_CACHE_LOCK.synchronize { @_shared_template_cache ||= LockedTemplateCache.new }
52
+ end
53
+
12
54
  def initialize_with_app(*args, &block)
13
- @template_cache = Sinatra::TemplateCache.new
14
55
  # Check if we have an app passed into initialize
15
56
  if args[0].respond_to?(:views) && args[0].respond_to?(:reload_templates?)
16
57
  app = args.shift
17
58
  @_views = File.join(app.views, 'mailers')
18
- settings.reload_templates = app.reload_templates?
59
+ settings.reload_templates = self.reload_templates = app.reload_templates?
19
60
  else
20
61
  @_views = File.expand_path('./mailers')
21
- settings.reload_templates = true
62
+ settings.reload_templates = self.reload_templates = true
22
63
  end
23
64
 
24
65
  initialize_template_settings!
@@ -109,7 +150,7 @@ module Mail # @private
109
150
  part_block = params.delete(:part_block)
110
151
  new_part = Mail::Part.new(params)
111
152
  new_part.views(@_views)
112
- new_part.settings.reload_templates = settings.reload_templates?
153
+ new_part.settings.reload_templates = new_part.reload_templates = reload_templates?
113
154
  new_part.instance_eval(&part_block) if part_block
114
155
  yield new_part if block_given?
115
156
  add_part(new_part)
@@ -151,6 +192,23 @@ module Mail # @private
151
192
  @_mailer_views = false
152
193
  end
153
194
 
195
+ ##
196
+ # Returns true if this message reads its templates again on every render, as its app's
197
+ # reload_templates setting said when the message was built.
198
+ #
199
+ def reload_templates?
200
+ @_reload_templates
201
+ end
202
+
203
+ ##
204
+ # Sets whether this message reads its templates again on every render. A message that does
205
+ # keeps every template to itself; one that doesn't shares the templates it renders by name.
206
+ #
207
+ def reload_templates=(value)
208
+ @_reload_templates = value
209
+ @template_cache = value ? Sinatra::TemplateCache.new : SharedTemplateCache.new(self.class)
210
+ end
211
+
154
212
  ##
155
213
  # Sets the local variables available within the message template.
156
214
  #
@@ -276,7 +334,7 @@ module Mail # @private
276
334
  #
277
335
  def render(engine = nil, data = nil, options = {}, locals = {}, &block)
278
336
  locals = @_locals || {} if !options[:locals] && locals.empty?
279
- @template_cache.clear if settings.reload_templates?
337
+ @template_cache.clear if reload_templates?
280
338
 
281
339
  # render(nil, 'template', ...) as RenderHelpers#partial calls it: shift the arguments
282
340
  # the way Padrino::Rendering#render does, before defaulting to the message name.
data/test/helper.rb CHANGED
@@ -2,6 +2,11 @@ ENV['RACK_ENV'] = 'test'
2
2
  PADRINO_ROOT = __dir__ unless defined? PADRINO_ROOT
3
3
 
4
4
  require 'minitest/autorun'
5
+ begin
6
+ require 'minitest/mock'
7
+ rescue LoadError
8
+ # minitest/mock is built-in for older minitest versions
9
+ end
5
10
  require 'minitest/pride'
6
11
  require 'rack/test'
7
12
  require 'padrino-core'
data/test/test_email.rb CHANGED
@@ -259,6 +259,84 @@ describe 'Email' do
259
259
  assert_equal 'B[beta hello]', @app.deliver(:beta, :boxed).body.to_s.strip
260
260
  end
261
261
 
262
+ it 'should compile a mailer template once for all its deliveries' do
263
+ mailer_app(mailer_views('site'))
264
+ @app.set :reload_templates, false
265
+ compiled = 0
266
+ engine = Tilt[:erb]
267
+ new_template = engine.method(:new)
268
+ engine.stub(:new, lambda { |*args, &block|
269
+ compiled += 1
270
+ new_template.call(*args, &block)
271
+ }) do
272
+ 2.times { assert_equal 'alpha hello', @app.deliver(:alpha, :hello).body.to_s }
273
+ end
274
+ assert_equal 1, compiled
275
+ end
276
+
277
+ it 'should read an edited mailer template when templates reload' do
278
+ views = mailer_views('site')
279
+ mailer_app(views)
280
+ @app.set :reload_templates, false
281
+ assert_equal 'alpha hello', @app.deliver(:alpha, :hello).body.to_s
282
+ File.write(File.join(views, 'mailers', 'alpha', 'hello.erb'), 'alpha edited')
283
+ @app.set :reload_templates, true
284
+ assert_equal 'alpha edited', @app.deliver(:alpha, :hello).body.to_s
285
+ end
286
+
287
+ it 'should read an edited part template when templates reload' do
288
+ views = mailer_views('site')
289
+ mailer_app(views)
290
+ @app.set :reload_templates, true
291
+ assert_equal 'gamma plain', @app.deliver(:gamma, :hello).parts.first.body.to_s
292
+ File.write(File.join(views, 'mailers', 'gamma', 'hello.plain.erb'), 'gamma plain edited')
293
+ assert_equal 'gamma plain edited', @app.deliver(:gamma, :hello).parts.first.body.to_s
294
+ end
295
+
296
+ it 'should compile part templates once for all their deliveries' do
297
+ mailer_app(mailer_views('site'))
298
+ @app.set :reload_templates, false
299
+ compiled = 0
300
+ engine = Tilt[:erb]
301
+ new_template = engine.method(:new)
302
+ engine.stub(:new, lambda { |*args, &block|
303
+ compiled += 1
304
+ new_template.call(*args, &block)
305
+ }) do
306
+ 2.times do
307
+ assert_equal(['gamma plain', 'gamma html'], @app.deliver(:gamma, :hello).parts.map { |part| part.body.to_s })
308
+ end
309
+ end
310
+ assert_equal 2, compiled
311
+ end
312
+
313
+ it 'should not dump the templates other messages share' do
314
+ mailer_app(mailer_views('site'))
315
+ @app.set :reload_templates, false
316
+ @app.deliver(:beta, :hello)
317
+ message = build_message(:alpha)
318
+ assert_equal 'alpha hello', message.send(:render, :hello)
319
+ yaml = message.to_yaml
320
+ refute_includes yaml, 'beta hello'
321
+ refute_includes yaml, 'Mutex'
322
+ end
323
+
324
+ it 'should keep the shared templates when a message of another app reloads them' do
325
+ views = mailer_views('site')
326
+ cached = mailer_app(views)
327
+ cached.set :reload_templates, false
328
+ reloading = mailer_app(views)
329
+ reloading.set :reload_templates, true
330
+ assert_equal 'alpha hello', cached.deliver(:alpha, :hello).body.to_s
331
+ File.write(File.join(views, 'mailers', 'alpha', 'hello.erb'), 'alpha edited')
332
+ @app = cached
333
+ waiting = build_message(:alpha)
334
+ @app = reloading
335
+ assert_equal 'alpha edited', build_message(:alpha).send(:render, :hello)
336
+ assert_equal 'alpha hello', waiting.send(:render, :hello)
337
+ assert_equal 'alpha hello', cached.deliver(:alpha, :hello).body.to_s
338
+ end
339
+
262
340
  MailerFormUser = Struct.new(:name) unless defined?(MailerFormUser)
263
341
 
264
342
  it 'should build forms with form_for in a mailer template' do
data/test/test_message.rb CHANGED
@@ -212,4 +212,33 @@ describe 'Message' do
212
212
  end
213
213
  end
214
214
  end
215
+
216
+ describe 'the shared template cache' do
217
+ before { @cache = Mail::Message::LockedTemplateCache.new }
218
+
219
+ it 'should give threads that compile a template at the same time the one it keeps' do
220
+ threads = Array.new(2) do
221
+ Thread.new do
222
+ @cache.fetch(:erb, :same) do
223
+ sleep 0.05
224
+ Object.new
225
+ end
226
+ end
227
+ end
228
+ kept = threads.map(&:value)
229
+ assert_same kept[0], kept[1]
230
+ assert_same(kept[0], @cache.fetch(:erb, :same) { flunk 'compiled a stored template again' })
231
+ end
232
+
233
+ it 'should serve other templates while one compiles' do
234
+ release = Queue.new
235
+ slow = Thread.new { @cache.fetch(:erb, :slow) { release.pop && :slow } }
236
+ Thread.pass while slow.status == 'run'
237
+ fast = Thread.new { @cache.fetch(:erb, :fast) { :fast } }
238
+ assert_equal :fast, fast.join(2) && fast.value
239
+ ensure
240
+ release << true
241
+ assert_equal :slow, slow.value
242
+ end
243
+ end
215
244
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: padrino-mailer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.2
4
+ version: 0.18.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Padrino Team
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-11-01 00:00:00.000000000 Z
14
+ date: 2026-11-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: padrino-core
@@ -19,14 +19,14 @@ dependencies:
19
19
  requirements:
20
20
  - - '='
21
21
  - !ruby/object:Gem::Version
22
- version: 0.18.2
22
+ version: 0.18.4
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - '='
28
28
  - !ruby/object:Gem::Version
29
- version: 0.18.2
29
+ version: 0.18.4
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: mime-types
32
32
  requirement: !ruby/object:Gem::Requirement