action_mailer-enqueable 1.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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in action_mailer-enqueable.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Eric Chapweske
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ # ActionMailer::Enqueable
2
+
3
+ Drop in support for using queues with existing delivery methods. Works with mailers that accept ActiveRecord, and simple JSON-compatible objects as arguments.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'action_mailer-enqueable'
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ class EnqueableMailer < ActionMailer::Base
15
+ extend ActionMailer::Enqueable
16
+
17
+ self.delivery_method = :test
18
+ self.queue = MailRenderingJob
19
+
20
+ def welcome(user)
21
+ recipients 'You'
22
+ from 'Me'
23
+
24
+ body "Email: Hello, #{user}"
25
+ end
26
+
27
+ end
28
+
29
+ class MailRenderingJob
30
+
31
+ def self.enqueue(deferred)
32
+ Resque.enqueue(deferred.encoded)
33
+ end
34
+
35
+ end
36
+ ````
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ #require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'test'
7
+ test.pattern = 'test/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/action_mailer/enqueable/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Eric Chapweske"]
6
+ gem.email = ["eac@zendesk.com"]
7
+ gem.description = "Serialize and enqueue deliveries for existing mailers"
8
+ gem.summary = %q{}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "action_mailer-enqueable"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ActionMailer::Enqueable::VERSION
17
+
18
+ gem.add_development_dependency 'activesupport', '2.3.16'
19
+ gem.add_development_dependency 'actionmailer', '2.3.16'
20
+ end
@@ -0,0 +1,27 @@
1
+ require 'action_mailer/enqueable/version'
2
+ require 'action_mailer/enqueable/deferred'
3
+ require 'active_support/core_ext/class/attribute'
4
+
5
+ module ActionMailer::Enqueable
6
+
7
+ def self.extended(base)
8
+ base.class_attribute :queue
9
+ end
10
+
11
+ def method_missing(method_symbol, *parameters) #:nodoc:
12
+ if match = matches_dynamic_method?(method_symbol)
13
+ if queue && match[1] == 'deliver'
14
+ enqueue(match[2], parameters)
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+
21
+ def enqueue(method_id, arguments)
22
+ deferred = Deferred.new(:mailer_name => name, :method_id => method_id, :arguments => arguments)
23
+ queue.enqueue(deferred)
24
+ deferred
25
+ end
26
+
27
+ end
@@ -0,0 +1,86 @@
1
+ require 'action_mailer/enqueable/record_encoder'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module ActionMailer::Enqueable
5
+ class Deferred
6
+ class Invalid < ArgumentError
7
+ end
8
+
9
+ def self.from_json(params)
10
+ from_hash(ActiveSupport::JSON.decode(params))
11
+ end
12
+
13
+ def self.from_hash(params)
14
+ decoded = encoder.decode(params)
15
+
16
+ new(decoded)
17
+ end
18
+
19
+ def self.encoder
20
+ RecordEncoder
21
+ end
22
+
23
+ attr_reader :errors, :params
24
+
25
+ def initialize(params)
26
+ @params = params
27
+ @params.symbolize_keys!
28
+ end
29
+
30
+ # ActionMailer does some bizzare stuff with #new.
31
+ def mailer
32
+ @mailer ||= begin
33
+ mailer = mailer_class.allocate
34
+ mailer.send(:initialize, method_id, *arguments)
35
+ mailer
36
+ end
37
+ end
38
+
39
+ def mailer_class
40
+ mailer_name.constantize
41
+ end
42
+
43
+ def mailer_name
44
+ params[:mailer_name]
45
+ end
46
+
47
+ def method_id
48
+ params[:method_id]
49
+ end
50
+
51
+ def arguments
52
+ params[:arguments]
53
+ end
54
+
55
+ def attributes
56
+ { :mailer_name => mailer_name, :method_id => method_id, :arguments => arguments }
57
+ end
58
+
59
+ def validate!
60
+ valid? || raise(Invalid.new("Deferred mailer is invalid: #{errors.inspect}"))
61
+ end
62
+
63
+ def valid?
64
+ @errors = attributes.map do |name, value|
65
+ "#{name} can't be nil" if value.nil?
66
+ end
67
+ @errors.compact!
68
+
69
+ @errors.empty?
70
+ end
71
+
72
+ def to_json(options = {})
73
+ ActiveSupport::JSON.encode(encoded)
74
+ end
75
+
76
+ def encoded
77
+ self.class.encoder.encode(params)
78
+ end
79
+
80
+ def ==(other)
81
+ other.respond_to?(:params) &&
82
+ other.params == params
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,76 @@
1
+ require 'active_support'
2
+
3
+ # Prepare ActiveRecord objects for safe serialization.
4
+ #
5
+ # user = User.first
6
+ # => #<User id: 1, account_id: 1, name: 'Buddhy'>
7
+ # RecordEncoder.encode(user)
8
+ # => { :class => 'User', :id => 1 }
9
+ # RecordEncoder.decode(user)
10
+ # => #<User id: 1, account_id: 1, name: 'Buddhy'>
11
+ #
12
+ module ActionMailer::Enqueable::RecordEncoder
13
+ class RecordIdMissingError < ArgumentError
14
+ def initialize(record)
15
+ @record = record
16
+ end
17
+
18
+ def message
19
+ "ActiveRecords must have an id to be serialized: (#{@record.inspect})"
20
+ end
21
+ end
22
+ CLASS = 'class'.freeze
23
+ ID = 'id'.freeze
24
+
25
+ extend self
26
+
27
+ def encode(params)
28
+ encoded = params.map do |argument|
29
+ case argument
30
+ when ActiveRecord::Base
31
+ encode_active_record(argument)
32
+ when Array, Hash
33
+ encode(argument)
34
+ when NilClass, TrueClass, FalseClass, Numeric, String, Symbol
35
+ argument
36
+ else
37
+ raise ArgumentError.new("Cannot encode #{argument} (#{argument.class})")
38
+ end
39
+ end
40
+
41
+ params.is_a?(Hash) ? Hash[encoded] : encoded
42
+ end
43
+
44
+ def decode(params)
45
+ decoded = params.map do |argument|
46
+ case argument
47
+ when Hash
48
+ active_record?(argument) ? decode_active_record(argument) : decode(argument)
49
+ when Array
50
+ decode(argument)
51
+ else
52
+ argument
53
+ end
54
+ end
55
+
56
+ params.is_a?(Hash) ? Hash[decoded] : decoded
57
+ end
58
+
59
+ protected
60
+
61
+ def decode_active_record(params)
62
+ params[CLASS].constantize.find(params[ID])
63
+ end
64
+
65
+ def active_record?(params)
66
+ params[CLASS] && params[ID]
67
+ end
68
+
69
+ def encode_active_record(record)
70
+ raise RecordIdMissingError.new(record) if record.id.to_s.empty?
71
+
72
+ { CLASS => record.class.name, ID => record.id }
73
+ end
74
+
75
+ end
76
+
@@ -0,0 +1,5 @@
1
+ module ActionMailer
2
+ module Enqueable
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,130 @@
1
+ require_relative 'helper'
2
+ require 'action_mailer/enqueable/deferred'
3
+
4
+ module ActiveRecord
5
+ class ActiveRecord::Base
6
+ end
7
+ end
8
+
9
+ class DeferredTest < MiniTest::Unit::TestCase
10
+
11
+ class User < ActiveRecord::Base
12
+
13
+ def self.find(id)
14
+ all.detect { |record| record.id == id }
15
+ end
16
+
17
+ def self.all
18
+ @all ||= [ User.new(:id => 1, :name => 'Squirrel'),
19
+ User.new(:id => 2, :name => 'Raccoon'),
20
+ User.new(:id => 3, :name => 'Bear') ]
21
+ end
22
+
23
+ attr_accessor :id, :name
24
+
25
+ def initialize(attributes)
26
+ self.id = attributes[:id]
27
+ self.name = attributes[:name]
28
+ end
29
+
30
+ end
31
+
32
+ class DeferredMailer
33
+
34
+ def delivery_welcome(message, attributes)
35
+ "#{message} #{attributes['user'].join(',')}"
36
+ end
37
+
38
+ end
39
+
40
+ describe 'Deferred' do
41
+ before do
42
+ @deferred = ActionMailer::Enqueable::Deferred.new(
43
+ 'mailer_name' => 'DeferredMailer',
44
+ 'method_id' => 'welcome',
45
+ 'arguments' => [ 'Hello!', { 'users' => User.all } ])
46
+ end
47
+
48
+ it 'is equal to another deferred object when all params are equal' do
49
+ other = ActionMailer::Enqueable::Deferred.new(
50
+ 'mailer_name' => 'DeferredMailer',
51
+ 'method_id' => 'welcome',
52
+ 'arguments' => [ 'Hello!', { 'users' => User.all } ])
53
+
54
+ assert_equal @deferred, other
55
+ end
56
+
57
+ describe 'valid?' do
58
+
59
+ it 'is false when :mailer_name, :method_id or :arguments are missing' do
60
+ @deferred.params.delete(:mailer_name)
61
+ assert_equal false, @deferred.valid?
62
+ end
63
+
64
+ it 'is true when :mailer_name, :method_id, and :arguments are all present' do
65
+ assert_equal true, @deferred.valid?
66
+ end
67
+
68
+ it 'populates errors' do
69
+ assert_equal nil, @deferred.errors
70
+ @deferred.valid?
71
+ assert_equal [], @deferred.errors
72
+
73
+ @deferred.params.delete(:mailer_name)
74
+ @deferred.valid?
75
+ assert_equal [ "mailer_name can't be nil" ], @deferred.errors
76
+ end
77
+
78
+ end
79
+
80
+ describe 'validate!' do
81
+
82
+ it 'raises an error when invalid' do
83
+ @deferred.params.delete(:mailer_name)
84
+ assert_equal false, @deferred.valid?
85
+ assert_raises(ActionMailer::Enqueable::Deferred::Invalid) { @deferred.validate! }
86
+ end
87
+
88
+ it 'does not raise an error when valid' do
89
+ assert_equal true, @deferred.valid?
90
+ assert @deferred.validate!
91
+ end
92
+ end
93
+
94
+ describe 'to_json' do
95
+
96
+ it 'converts ActiveRecord objects into an easily deserializable form' do
97
+ encoded = '{"mailer_name":"DeferredMailer","method_id":"welcome","arguments":["Hello!",{"users":[{"class":"DeferredTest::User","id":1},{"class":"DeferredTest::User","id":2},{"class":"DeferredTest::User","id":3}]}]}'
98
+ assert_equal encoded, @deferred.to_json
99
+ end
100
+
101
+ end
102
+
103
+ describe 'from_json' do
104
+
105
+ it 'deserializes correctly' do
106
+ decoded = ActionMailer::Enqueable::Deferred.from_json(@deferred.to_json)
107
+ assert_equal 'DeferredMailer', decoded.mailer_name
108
+ assert_equal 'welcome', decoded.method_id
109
+ assert_equal [ 'Hello!', { 'users' => User.all } ], decoded.arguments
110
+ end
111
+
112
+ end
113
+
114
+ describe 'RecordEncoder' do
115
+
116
+ it "throws an exception when encoding records that can't be decoded" do
117
+ record = User.new(:id => nil)
118
+ exception = ActionMailer::Enqueable::RecordEncoder::RecordIdMissingError
119
+ encoder = ActionMailer::Enqueable::RecordEncoder
120
+
121
+ assert_raises(exception) { encoder.encode({ :record => record }) }
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+
128
+ end
129
+
130
+
@@ -0,0 +1,55 @@
1
+ require_relative 'helper'
2
+ require 'action_mailer'
3
+ require 'action_mailer/enqueable'
4
+
5
+ class EnqueableTest < MiniTest::Unit::TestCase
6
+
7
+ class EnqueableMailer < ActionMailer::Base
8
+ extend ActionMailer::Enqueable
9
+
10
+ self.delivery_method = :test
11
+
12
+ def welcome(user)
13
+ recipients 'You'
14
+ from 'Me'
15
+
16
+ body "Email: Hello, #{user}"
17
+ end
18
+
19
+ end
20
+
21
+ class Queue < Array
22
+ alias enqueue push
23
+ end
24
+
25
+ describe 'Mailer with a queue' do
26
+ before do
27
+ @queue = Queue.new
28
+ EnqueableMailer.queue = @queue
29
+ end
30
+
31
+ it 'enqueues messages instead of deliverying them' do
32
+ deferred = EnqueableMailer.deliver_welcome('Buddhy')
33
+ assert_equal EnqueableMailer, deferred.mailer_class
34
+ assert_equal 'welcome', deferred.method_id
35
+ assert_equal [ 'Buddhy' ], deferred.arguments
36
+
37
+ deferred = ActionMailer::Enqueable::Deferred.new(:mailer_name => 'EnqueableTest::EnqueableMailer', :method_id => 'welcome', :arguments => [ 'Buddhy' ] )
38
+ assert_equal [ deferred ], @queue
39
+ end
40
+
41
+ end
42
+
43
+ describe 'Mailer without a queue' do
44
+ before do
45
+ EnqueableMailer.queue = nil
46
+ end
47
+
48
+ it 'delivers messages without attempting to enqueue' do
49
+ mail = EnqueableMailer.deliver_welcome('Buddhy')
50
+ assert_equal 'Email: Hello, Buddhy', mail.body
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,3 @@
1
+ require 'bundler/setup'
2
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '../lib'))) # lib
3
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: action_mailer-enqueable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Eric Chapweske
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.3.16
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.3.16
30
+ - !ruby/object:Gem::Dependency
31
+ name: actionmailer
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 2.3.16
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 2.3.16
46
+ description: Serialize and enqueue deliveries for existing mailers
47
+ email:
48
+ - eac@zendesk.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - action_mailer-enqueable.gemspec
59
+ - lib/action_mailer/enqueable.rb
60
+ - lib/action_mailer/enqueable/deferred.rb
61
+ - lib/action_mailer/enqueable/record_encoder.rb
62
+ - lib/action_mailer/enqueable/version.rb
63
+ - test/deferred_test.rb
64
+ - test/enqueable_test.rb
65
+ - test/helper.rb
66
+ homepage: ''
67
+ licenses: []
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 1.8.21
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: ''
90
+ test_files:
91
+ - test/deferred_test.rb
92
+ - test/enqueable_test.rb
93
+ - test/helper.rb