outbox 0.0.1 → 0.1.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.
data/.gitignore CHANGED
@@ -2,6 +2,7 @@
2
2
  *.rbc
3
3
  .bundle
4
4
  .config
5
+ .ruby-version
5
6
  .yardoc
6
7
  Gemfile.lock
7
8
  InstalledFiles
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  Outbox
2
2
  ======
3
3
 
4
- Outbox is a Rails Engine and generic interface for sending notifications using a variety of delivery methods, with built-in support for the most popular SaaS solutions.
4
+ [![Gem Version](https://badge.fury.io/rb/outbox.png)](http://badge.fury.io/rb/outbox)
5
+
6
+ Outbox is a factory for creating notifications in a variety of protocols, including: email, SMS, and push notifications. Each protocol is built as a generic interface where the actual delivery method or service can be configured independently of the message itself.
7
+
5
8
 
6
9
  Installation
7
10
  ------------
@@ -24,10 +27,88 @@ Or install it yourself as:
24
27
  $ gem install outbox
25
28
  ```
26
29
 
30
+ Support
31
+ -------
32
+
33
+ This gem is still in early development with plans to support email, SMS, and push notificaitons. As protocols and services are added, this support table will be updated:
34
+
35
+ ### Email
36
+
37
+ | Service | Alias | Client |
38
+ |-------------------------------------------|---------|------------|
39
+ | [Mail gem](https://github.com/mikel/mail) | `:mail` | MailClient |
40
+
41
+ ### SMS
42
+
43
+ TODO…
44
+
45
+ ### Push
46
+
47
+ TODO…
48
+
27
49
  Usage
28
50
  -----
29
51
 
30
- TODO: Write usage instructions here
52
+ Outbox is inspired by [Mail's](https://github.com/mikel/mail) syntax for creating emails.
53
+
54
+ ### Making a Message
55
+
56
+ An Outbox message is actually a factory for creating many different types of messages with the same **topic**. For example: a **topic** could be an event reminder in a calendar application. You want to send out essentially the same content (the reminder) as an email, SMS, and/or push notifications depending on user preferences:
57
+
58
+ ``` ruby
59
+ message = Outbox::Message.new do
60
+ email do
61
+ from 'noreply@myapp.com'
62
+ subject 'You have an upcoming event!'
63
+ end
64
+
65
+ sms do
66
+ from '+15557654321'
67
+ end
68
+
69
+ ios_push do
70
+ badget '+1'
71
+ sound 'default'
72
+ end
73
+
74
+ body "Don't forget, you have an upcoming event on 8/15/2013."
75
+ end
76
+
77
+ # This will deliver the message to User's given contact points.
78
+ message.deliver email: 'user@gmail.com', sms: '+15551234567', ios_push: 'FE66489F304DC75B8D6E8200DFF8A456E8DAEACEC428B427E9518741C92C6660'
79
+ ```
80
+
81
+ ### Making an email
82
+
83
+ Making just an email is done just how you would using the [Mail gem](https://github.com/mikel/mail), so look there for in-depth examples. Here's a simple one to get you started:
84
+
85
+ ``` ruby
86
+ email = Outbox::Messages::Email.new do
87
+ to 'user@gmail.com'
88
+ from 'noreply@myapp.com'
89
+ subject 'You have an upcoming event!'
90
+
91
+ text_part do
92
+ body "Don't forget, you have an upcoming event on 8/15/2013."
93
+ end
94
+
95
+ html_part do
96
+ body "<h1>Event Reminder</h1>..."
97
+ end
98
+ end
99
+
100
+ # Configure the client. If you use the MailClient, you can specify
101
+ # the actual delivery method:
102
+ email.client :mail, delivery_method: :smtp, smtp_settings: {}
103
+
104
+ # And deliver using the specified client
105
+ email.deliver
106
+ ```
107
+
108
+ Configuration
109
+ -------------
110
+
111
+ TODO...
31
112
 
32
113
  Contributing
33
114
  ------------
@@ -1,4 +1,21 @@
1
- require 'outbox/version'
2
-
3
1
  module Outbox
2
+ require 'outbox/errors'
3
+ require 'outbox/version'
4
+
5
+ autoload 'Accessor', 'outbox/accessor'
6
+ autoload 'Message', 'outbox/message'
7
+ autoload 'MessageClients', 'outbox/message_clients'
8
+ autoload 'MessageFields', 'outbox/message_fields'
9
+ autoload 'MessageTypes', 'outbox/message_types'
10
+
11
+ module Clients
12
+ autoload 'Base', 'outbox/clients/base'
13
+ autoload 'MailClient', 'outbox/clients/mail_client'
14
+ autoload 'TestClient', 'outbox/clients/test_client'
15
+ end
16
+
17
+ module Messages
18
+ autoload 'Base', 'outbox/messages/base'
19
+ autoload 'Email', 'outbox/messages/email'
20
+ end
4
21
  end
@@ -0,0 +1,71 @@
1
+ module Outbox
2
+ # Accessor is a simple object for wrapping access to either a hash's keys
3
+ # or an object's properties. You can arbitrarily get/set either. Note that
4
+ # with hashes, the keys are symbolized and a new hash is created - so if you
5
+ # set properties you'll need to get the resulting hash from the #object
6
+ # method.
7
+ #
8
+ # Example:
9
+ #
10
+ # hash = { :a => 1, 'b' => 2 }
11
+ # hash_accessor = Outbox::Accessor.new(hash)
12
+ # hash_accessor[:a] #=> 1
13
+ # hash_accessor[:b] #=> 2
14
+ # hash_accessor[:c] #=> nil
15
+ # hash_accessor[:c] = 3
16
+ # hash_accessor.object[:c] #=> 3
17
+ # hash_accessor.object #=> { a: 1, b: 2, c: 3 }
18
+ #
19
+ # object = OpenStruct.new
20
+ # object.a = 1
21
+ # object.b = 2
22
+ # object_accessor = Outbox::Accessor.new(object)
23
+ # object_accessor[:a] #=> 1
24
+ # object_accessor[:b] #=> 2
25
+ # object_accessor[:c] #=> nil
26
+ # object_accessor[:c] = 3
27
+ # object_accessor.object.c #=> 3
28
+ class Accessor
29
+ attr_reader :object
30
+
31
+ def initialize(object)
32
+ if object.instance_of? Hash
33
+ @object = convert_keys(object)
34
+ else
35
+ @object = object
36
+ end
37
+ end
38
+
39
+ def []=(key, value)
40
+ setter = "#{key}="
41
+ if @object.respond_to?(setter)
42
+ @object.public_send(setter, value)
43
+ elsif @object.respond_to? :[]=
44
+ @object[convert_key(key)] = value
45
+ end
46
+ end
47
+
48
+ def [](key)
49
+ key = convert_key(key)
50
+ if @object.respond_to?(key)
51
+ @object.public_send(key)
52
+ elsif @object.respond_to? :[]
53
+ @object[key]
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def convert_keys(hash)
60
+ result = {}
61
+ hash.each_key do |key|
62
+ result[convert_key(key)] = hash[key]
63
+ end
64
+ result
65
+ end
66
+
67
+ def convert_key(key)
68
+ key.to_sym rescue key
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ module Outbox
2
+ module Clients
3
+ class Base
4
+ attr_reader :settings
5
+
6
+ # Sets default settings for the client.
7
+ #
8
+ # MailClient.defaults delivery_method: :sendmail
9
+ # client = MailClient.new
10
+ # client.settings[:delivery_method] #=> :sendmail
11
+ def self.defaults(defaults = nil)
12
+ @defaults ||= {}
13
+
14
+ if defaults.nil?
15
+ @defaults
16
+ else
17
+ @defaults.merge!(defaults)
18
+ end
19
+ end
20
+
21
+ # Creates a new client instance. Settings can be configured per
22
+ # instance by passing in a hash.
23
+ #
24
+ # client = MailClient.new delivery_method: :sendmail
25
+ # client.settings[:delivery_method] #=> :sendmail
26
+ def initialize(settings = nil)
27
+ @settings = self.class.defaults.dup
28
+ @settings.merge! settings if settings
29
+ end
30
+
31
+ # Delivers the given message.
32
+ #
33
+ # Subclasses must provide an implementation of this method.
34
+ def deliver(message)
35
+ raise NotImplementedError, 'Subclasses must implement a deliver method'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ module Outbox
2
+ module Clients
3
+ class MailClient < Base
4
+ defaults delivery_method: :smtp
5
+
6
+ # Returns the configured delivery method.
7
+ def delivery_method
8
+ settings[:delivery_method]
9
+ end
10
+
11
+ # Returns the configured delivery method settings. This will also check
12
+ # the Rails-style #{delivery_method}_settings key as well.
13
+ #
14
+ # client = Outbox::Clients::MailClient.new delivery_method: :sendmail, delivery_method_settings: {
15
+ # location: '/usr/bin/sendmail'
16
+ # }
17
+ # client.delivery_method_settings #=> { location: '/usr/bin/sendmail' }
18
+ #
19
+ # client = Outbox::Clients::MailClient.new delivery_method: :sendmail, sendmail_settings: {
20
+ # location: '/usr/bin/sendmail'
21
+ # }
22
+ # client.delivery_method_settings #=> { location: '/usr/bin/sendmail' }
23
+ def delivery_method_settings
24
+ settings[:delivery_method_settings] || settings[:"#{delivery_method}_settings"]
25
+ end
26
+
27
+ def deliver(email)
28
+ message = create_message_from_email(email)
29
+ message.delivery_method(delivery_method, delivery_method_settings)
30
+ message.deliver
31
+ end
32
+
33
+ private
34
+
35
+ def create_message_from_email(email)
36
+ email.message_object.dup
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Outbox
2
+ module Clients
3
+ # The TestClient is a bare bones client that does nothing. It is useful
4
+ # when you are testing.
5
+ #
6
+ # It also provides a template of the minimum methods required to make
7
+ # a custom client.
8
+ class TestClient < Base
9
+ # Provides a store of all the message sent with the TestClient so you
10
+ # can check them.
11
+ def self.deliveries
12
+ @@deliveries ||= []
13
+ end
14
+
15
+ def deliver(message)
16
+ self.class.deliveries << message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Outbox
2
+ # Raised when a message is missing data for a required field.
3
+ class MissingRequiredFieldError < StandardError
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ module Outbox
2
+ class Message
3
+ include MessageTypes
4
+
5
+ register_message_type :email, Outbox::Messages::Email
6
+
7
+ # Make a new message. Every message can be created using a hash,
8
+ # block, or direct assignment.
9
+ #
10
+ # message = Message.new do
11
+ # email do
12
+ # subject 'Subject'
13
+ # end
14
+ # end
15
+ # message = Message.new email: { subject: 'Subject' }
16
+ # message = Message.new
17
+ # message.email = Email.new subject: 'Subject'
18
+ def initialize(message_type_values = nil, &block)
19
+ if block_given?
20
+ instance_eval(&block)
21
+ else
22
+ assign_message_type_values(message_type_values) unless message_type_values.nil?
23
+ end
24
+ end
25
+
26
+ # Delivers all of the messages to the given 'audience'. An 'audience' object
27
+ # can be a hash or an object that responds to the current message types. Only
28
+ # the message types specified in the 'audience' object will be sent to.
29
+ #
30
+ # message.deliver email: 'hello@example.com', sms: '+15555555555'
31
+ # audience = OpenStruct.new
32
+ # audience.email = 'hello@example.com'
33
+ # audience.sms = '+15555555555'
34
+ # message.deliver(audience)
35
+ def deliver(audience)
36
+ audience = Outbox::Accessor.new(audience)
37
+
38
+ self.class.message_types.each_key do |message_type|
39
+ message = self.public_send(message_type)
40
+ next if message.nil?
41
+
42
+ recipient = audience[message_type]
43
+ message.deliver(recipient) if recipient
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,70 @@
1
+ module Outbox
2
+ module MessageClients
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Returns the default client for the message type.
9
+ #
10
+ # Email.default_client #=> #<Outbox::Clients::Mail>
11
+ #
12
+ # Also allows you to set the default client using an alias, with optoins.
13
+ #
14
+ # Email.default_client :test, option: 'foo'
15
+ # Email.default_client #=> #<Outbox::Clients::TestClient>
16
+ def default_client(client = nil, options = nil)
17
+ if client.nil?
18
+ @default_client
19
+ else
20
+ @default_client = get_client(client, options)
21
+ end
22
+ end
23
+
24
+ # Registers a client class with an alias.
25
+ #
26
+ # Email.register_client_alias :mandrill, MandrillClient
27
+ # Email.default_client :mandrill, mandrill_option: 'foo'
28
+ def register_client_alias(name, client)
29
+ registered_client_aliases[name.to_sym] = client
30
+ end
31
+
32
+ # Returns a hash of client aliases, where the key is the alias and
33
+ # the value is client class.
34
+ def registered_client_aliases
35
+ @registered_client_aliases ||= { test: Outbox::Clients::TestClient }
36
+ end
37
+
38
+ protected
39
+
40
+ def get_client(client, options = nil)
41
+ case client
42
+ when Symbol, String
43
+ client = registered_client_aliases[client.to_sym]
44
+ end
45
+
46
+ if client.instance_of?(Class)
47
+ client.new(options)
48
+ else
49
+ client
50
+ end
51
+ end
52
+ end
53
+
54
+ # Returns the message's client.
55
+ #
56
+ # message.client #=> #<Outbox::Clients::Mail>
57
+ #
58
+ # Also allows you set the instance's client using an alias, with options.
59
+ #
60
+ # message.client :test, option: 'foo'
61
+ # message.client #=> #<Outbox::Clients::TestClient>
62
+ def client(client = nil, options = nil)
63
+ if client.nil?
64
+ @client
65
+ else
66
+ @client = self.class.send(:get_client, client, options)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,209 @@
1
+ module Outbox
2
+ module MessageFields
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ # Sets default values for defined fields.
9
+ #
10
+ # Email.defaults from: 'bob@example.com'
11
+ # message = Email.new
12
+ # message.from #=> 'bob@example.com'
13
+ def defaults(defaults = nil)
14
+ @defaults ||= {}
15
+
16
+ if defaults.nil?
17
+ @defaults
18
+ else
19
+ @defaults.merge!(defaults)
20
+ end
21
+ end
22
+
23
+ # Returns the defined fields for this message type.
24
+ #
25
+ # class SomeMessageType < Outbox::Messages::Base
26
+ # field :to
27
+ # field :from
28
+ # end
29
+ #
30
+ # SomeMessageType.fields #=> [:to, :from]
31
+ #
32
+ # Also allows you to define multiple fields at once.
33
+ #
34
+ # class SomeMessageType < Outbox::Messages::Base
35
+ # fields :to, :from, required: true
36
+ # end
37
+ #
38
+ # message = SomeMessageType.new do
39
+ # to 'Bob'
40
+ # from 'John'
41
+ # end
42
+ # message.to #=> 'Bob'
43
+ # message.from #=> 'John'
44
+ # message.from = nil
45
+ # message.validate_fields #=> raises Outbox::MissingRequiredFieldError
46
+ def fields(*names)
47
+ if names.empty?
48
+ @fields ||= []
49
+ else
50
+ options = names.last.is_a?(Hash) ? names.pop : {}
51
+ names.flatten.each do |name|
52
+ field(name, options)
53
+ end
54
+ end
55
+ end
56
+
57
+ # Defines a 'field' which is a point of data for this type of data.
58
+ # Optionally you can set it to be required, or wether or not you want
59
+ # accessors defined for you. If you define your own accessors, make
60
+ # sure the reader also accepts a value that can be set, so it'll work
61
+ # with the block definition.
62
+ #
63
+ # class SomeMessageType < Outbox::Messages::base
64
+ # field :to, required: true
65
+ # field :body, accessor: false
66
+ #
67
+ # def body(value = nil)
68
+ # value ? self.body = value : @body
69
+ # end
70
+ #
71
+ # def body=(value)
72
+ # @body = parse_body(value)
73
+ # end
74
+ # end
75
+ #
76
+ # message = SomeMessageType.new do
77
+ # to 'Bob'
78
+ # end
79
+ # message.to #=> 'Bob'
80
+ # message.to = 'John'
81
+ # message.to #=> 'John'
82
+ # message.to = nil
83
+ # message.validate_fields #=> raises Outbox::MissingRequiredFieldError
84
+ def field(name, options = {})
85
+ name = name.to_sym
86
+ options = Outbox::Accessor.new(options)
87
+
88
+ fields.push(name)
89
+ required_fields.push(name) if options[:required]
90
+
91
+ unless options[:accessor] == false
92
+ define_field_reader(name) unless options[:reader] == false
93
+ define_field_writer(name) unless options[:writer] == false
94
+ end
95
+ end
96
+
97
+ # Returns an array of the required fields for a message type.
98
+ #
99
+ # class SomeMessageType < Outbox::Messages::Base
100
+ # field :to, required: true
101
+ # fields :from, :subject
102
+ # end
103
+ # SomeMessageType.required_fields #=> [:to]
104
+ #
105
+ # Also can be used an alias for defining fields that are required.
106
+ #
107
+ # class SomeMessageType < Outbox::Messages::Base
108
+ # required_fields :to, :from
109
+ # end
110
+ # SomeMessageType.required_fields #=> [:to, :from]
111
+ def required_fields(*names)
112
+ if names.empty?
113
+ @required_fields ||= []
114
+ else
115
+ options = names.last.is_a?(Hash) ? names.pop : {}
116
+ options[:required] = true
117
+ names << options
118
+ fields(*names)
119
+ end
120
+ end
121
+ alias :required_field :required_fields
122
+
123
+ protected
124
+
125
+ def define_field_reader(name)
126
+ define_method(name) do |value = nil|
127
+ if value.nil?
128
+ @fields[name]
129
+ else
130
+ @fields[name] = value
131
+ end
132
+ end
133
+ end
134
+
135
+ def define_field_writer(name)
136
+ define_method("#{name}=") do |value|
137
+ @fields[name] = value
138
+ end
139
+ end
140
+ end
141
+
142
+ # Read an arbitrary field.
143
+ #
144
+ # Example:
145
+ #
146
+ # message['foo'] = '1234'
147
+ # message['foo'] #=> '1234'
148
+ def [](name)
149
+ @fields[name.to_sym]
150
+ end
151
+
152
+ # Add an arbitray field.
153
+ #
154
+ # Example:
155
+ #
156
+ # message['foo'] = '1234'
157
+ # message['foo'] #=> '1234'
158
+ def []=(name, value)
159
+ @fields[name.to_sym] = value
160
+ end
161
+
162
+ # Returns a hash of the defined fields.
163
+ #
164
+ # class SomeMessageType < Outbox::Messages::Base
165
+ # fields :to, :from
166
+ # end
167
+ # message = SomeMessageType.new to: 'Bob'
168
+ # message.from 'John'
169
+ # message.fields #=> { to: 'Bob', from: 'John' }
170
+ #
171
+ # Also allows you to set fields if you pass in a hash.
172
+ #
173
+ # message.fields to: 'Bob', from: 'Sally'
174
+ # message.fields #=> { to: 'Bob', from: 'Sally' }
175
+ def fields(new_fields = nil)
176
+ if new_fields.nil?
177
+ fields = {}
178
+ self.class.fields.each do |field|
179
+ fields[field] = self.public_send(field)
180
+ end
181
+ fields
182
+ else
183
+ self.fields = new_fields
184
+ end
185
+ end
186
+
187
+ # Assigns the values of the given hash.
188
+ #
189
+ # message.to = 'Bob'
190
+ # message.fields = { from: 'Sally' }
191
+ # message.fields #=> { to: 'Bob', from: 'Sally' }
192
+ def fields=(new_fields)
193
+ new_fields.each do |field, value|
194
+ self.public_send(field, value) if self.respond_to?(field)
195
+ end
196
+ end
197
+
198
+ # Checks the current values of the fields and raises errors for any
199
+ # validation issues.
200
+ def validate_fields
201
+ self.class.required_fields.each do |field|
202
+ value = self.public_send(field)
203
+ if value.nil? || value.respond_to?(:empty?) && value.empty?
204
+ raise Outbox::MissingRequiredFieldError.new("Missing required field: #{field}")
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end