bronto 0.0.1
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 +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +82 -0
- data/Rakefile +12 -0
- data/bronto.gemspec +23 -0
- data/lib/bronto.rb +36 -0
- data/lib/bronto/base.rb +198 -0
- data/lib/bronto/contact.rb +62 -0
- data/lib/bronto/delivery.rb +49 -0
- data/lib/bronto/field.rb +11 -0
- data/lib/bronto/filter.rb +36 -0
- data/lib/bronto/list.rb +62 -0
- data/lib/bronto/message.rb +36 -0
- data/lib/bronto/version.rb +3 -0
- data/lib/core_ext/array.rb +14 -0
- data/lib/core_ext/object.rb +34 -0
- data/lib/core_ext/string.rb +8 -0
- data/test/contact_test.rb +91 -0
- data/test/delivery_test.rb +98 -0
- data/test/field_test.rb +80 -0
- data/test/list_test.rb +136 -0
- data/test/message_test.rb +83 -0
- data/test/test_helper.rb +25 -0
- metadata +138 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Martin Gordon
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Bronto
|
2
|
+
|
3
|
+
This is a handy library that wraps the Bronto SOAP API.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'bronto'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install bronto
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Let's setup some contacts, add them to a list, and then send them a message.
|
22
|
+
|
23
|
+
1. Require the library and specify the API Key on the Base class:
|
24
|
+
|
25
|
+
```
|
26
|
+
require 'bronto'
|
27
|
+
Bronto::Base.api_key = "..."
|
28
|
+
```
|
29
|
+
|
30
|
+
2. Create two contacts:
|
31
|
+
|
32
|
+
```
|
33
|
+
contact_1 = Bronto::Contact.new(email: "test_1@example.com")
|
34
|
+
contact_2 = Bronto::Contact.new(email: "test_2@example.com")
|
35
|
+
|
36
|
+
puts contact_1.id
|
37
|
+
# => nil
|
38
|
+
|
39
|
+
puts contact_2.id
|
40
|
+
# => nil
|
41
|
+
|
42
|
+
# You can save multiple objects with one API call using the class `save` method.
|
43
|
+
Bronto::Contact.save(contact_1, contact_2)
|
44
|
+
|
45
|
+
# Both contacts should now have ids.
|
46
|
+
puts contact_1.id
|
47
|
+
# => "32da24c..."
|
48
|
+
|
49
|
+
puts contact_2.id
|
50
|
+
# => "98cd453..."
|
51
|
+
```
|
52
|
+
|
53
|
+
3. Create a list and the contacts:
|
54
|
+
|
55
|
+
```
|
56
|
+
list = Bronto::List.new(name: "A Test List", label: "This is a test list.")
|
57
|
+
list.save
|
58
|
+
|
59
|
+
list.add_to_list(contact_1, contact_2)
|
60
|
+
```
|
61
|
+
|
62
|
+
4. Create a new message and add content:
|
63
|
+
|
64
|
+
message = Bronto::Message.new(name: "Test Message")
|
65
|
+
message.add_content("html", "HTML Subject", "HTML Content")
|
66
|
+
message.add_content("text", "Text Subject", "Text Content")
|
67
|
+
message.save
|
68
|
+
|
69
|
+
5. Create a new delivery with a message and recipients and send it ASAP:
|
70
|
+
|
71
|
+
delivery = Bronto::Delivery.new(start: Time.now, type: "normal", from_name: "Test", from_email: "test@example.com")
|
72
|
+
delivery.message_id = message.id
|
73
|
+
delivery.add_recipient(list)
|
74
|
+
delivery.save
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
1. Fork it
|
79
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
80
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
81
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require "bundler/gem_tasks"
|
5
|
+
require "rake/testtask"
|
6
|
+
require "./lib/bronto"
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |test|
|
9
|
+
test.ruby_opts = ["-rubygems"] if defined? Gem
|
10
|
+
test.libs << "lib" << "test"
|
11
|
+
test.pattern = "test/**/*_test.rb"
|
12
|
+
end
|
data/bronto.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/bronto/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Martin Gordon"]
|
6
|
+
gem.email = ["martingordon@gmail.com"]
|
7
|
+
gem.description = %q{A Ruby wrapper for the Bronto SOAP API}
|
8
|
+
gem.summary = %q{A Ruby wrapper for the Bronto SOAP API}
|
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 = "bronto"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Bronto::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "savon"
|
19
|
+
|
20
|
+
gem.add_development_dependency "debugger"
|
21
|
+
gem.add_development_dependency "turn"
|
22
|
+
gem.add_development_dependency "shoulda"
|
23
|
+
end
|
data/lib/bronto.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require "savon"
|
2
|
+
|
3
|
+
require "bronto/base"
|
4
|
+
require "bronto/contact"
|
5
|
+
require "bronto/delivery"
|
6
|
+
require "bronto/field"
|
7
|
+
require "bronto/filter"
|
8
|
+
require "bronto/list"
|
9
|
+
require "bronto/message"
|
10
|
+
require "bronto/version"
|
11
|
+
|
12
|
+
require "core_ext/array"
|
13
|
+
require "core_ext/object"
|
14
|
+
require "core_ext/string"
|
15
|
+
|
16
|
+
module Bronto
|
17
|
+
class Errors
|
18
|
+
attr_accessor :messages
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
self.messages = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(code, message)
|
25
|
+
messages[code] = message
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear; messages.clear; end
|
29
|
+
def length; messages.length; end
|
30
|
+
def count; messages.count; end
|
31
|
+
|
32
|
+
def to_a(include_codes = true)
|
33
|
+
messages.map { |k,v| include_codes ? "#{v} (#{k})" : v }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/bronto/base.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
module Bronto
|
2
|
+
class Base
|
3
|
+
attr_accessor :id, :errors
|
4
|
+
|
5
|
+
# Getter/Setter for global API Key.
|
6
|
+
def self.api_key=(api_key)
|
7
|
+
@@api_key = api_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.api_key
|
11
|
+
@@api_key
|
12
|
+
end
|
13
|
+
|
14
|
+
# Simple helper method to convert class name to downcased pluralized version (e.g., Field -> fields).
|
15
|
+
def self.plural_class_name
|
16
|
+
self.to_s.split("::").last.downcase.pluralize
|
17
|
+
end
|
18
|
+
|
19
|
+
# The primary method used to interface with the SOAP API.
|
20
|
+
# This method automatically adds the required session header and returns the actual response section of the SOAP response body.
|
21
|
+
#
|
22
|
+
# If a symbol is passed in, it is converted to "method_plural_class_name" (e.g., :read => read_lists). A string
|
23
|
+
# method is used as-is.
|
24
|
+
# Pass in a block and assign a hash to soap.body with a structure appropriate to the method call.
|
25
|
+
def self.request(method, refresh_header = false, &_block)
|
26
|
+
_soap_header = self.soap_header(refresh_header)
|
27
|
+
|
28
|
+
method = "#{method}_#{plural_class_name}" if method.is_a? Symbol
|
29
|
+
|
30
|
+
resp = api.request(:v4, method.to_sym) do
|
31
|
+
soap.header = _soap_header
|
32
|
+
evaluate(&_block) if _block # See Savon::Client#evaluate; necessary to preserve scope.
|
33
|
+
end
|
34
|
+
|
35
|
+
resp.body["#{method}_response".to_sym]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets up the Savon SOAP client object (if necessary) and returns it.
|
39
|
+
def self.api
|
40
|
+
return @api unless @api.nil?
|
41
|
+
|
42
|
+
@api = Savon::Client.new do
|
43
|
+
wsdl.endpoint = "https://api.bronto.com/v4"
|
44
|
+
wsdl.namespace = "http://api.bronto.com/v4"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Helper method to retrieve the session ID and return a SOAP header.
|
49
|
+
# Will return a header with the same initial session ID unless the `refresh` argument is `true`.
|
50
|
+
def self.soap_header(refresh = false)
|
51
|
+
return @soap_header if !refresh and @soap_header.present?
|
52
|
+
|
53
|
+
resp = api.request(:v4, :login) do
|
54
|
+
soap.body = { api_token: self.api_key }
|
55
|
+
end
|
56
|
+
|
57
|
+
@soap_header = { "v4:sessionHeader" => { session_id: resp.body[:login_response][:return] } }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Saves a collection of Bronto::Base objects.
|
61
|
+
# Objects without IDs are considered new and are `create`d; objects with IDs are considered existing and are `update`d.
|
62
|
+
def self.save(*objs)
|
63
|
+
objs = objs.flatten
|
64
|
+
update(objs.select { |o| o.id.present? })
|
65
|
+
create(objs.select { |o| o.id.blank? })
|
66
|
+
objs
|
67
|
+
end
|
68
|
+
|
69
|
+
# Finds objects matching the `filter` (a Bronto::Filter instance).
|
70
|
+
def self.find(filter = Bronto::Filter.new, page_number = 1)
|
71
|
+
resp = request(:read) do
|
72
|
+
soap.body = { filter: filter.to_hash, page_number: page_number }
|
73
|
+
end
|
74
|
+
|
75
|
+
Array.wrap(resp[:return]).map { |hash| new(hash) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Tells the remote server to create the passed in collection of Bronto::Base objects.
|
79
|
+
# The object should implement `to_hash` to return a hash in the format expected by the SOAP API.
|
80
|
+
#
|
81
|
+
# Returns the same collection of objects that was passed in. Objects whose creation succeeded will be assigned the
|
82
|
+
# ID returned from Bronto.
|
83
|
+
def self.create(*objs)
|
84
|
+
objs = objs.flatten
|
85
|
+
|
86
|
+
resp = request(:add) do
|
87
|
+
soap.body = {
|
88
|
+
plural_class_name => objs.map(&:to_hash)
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
objs.each { |o| o.errors.clear }
|
93
|
+
|
94
|
+
Array.wrap(resp[:return][:results]).each_with_index do |result, i|
|
95
|
+
if result[:is_new] and !result[:is_error]
|
96
|
+
objs[i].id = result[:id]
|
97
|
+
elsif result[:is_error]
|
98
|
+
objs[i].errors.add(result[:error_code], result[:error_string])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
objs
|
103
|
+
end
|
104
|
+
|
105
|
+
# Updates a collection of Bronto::Base objects. The objects should exist on the remote server.
|
106
|
+
# The object should implement `to_hash` to return a hash in the format expected by the SOAP API.
|
107
|
+
def self.update(*objs)
|
108
|
+
objs = objs.flatten
|
109
|
+
|
110
|
+
resp = request(:update) do
|
111
|
+
soap.body = {
|
112
|
+
plural_class_name => objs.map(&:to_hash)
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
objs.each { |o| o.errors.clear }
|
117
|
+
objs
|
118
|
+
end
|
119
|
+
|
120
|
+
# Destroys a collection of Bronto::Base objects on the remote server.
|
121
|
+
#
|
122
|
+
# Returns the same collection of objects that was passed in. Objects whose destruction succeeded will
|
123
|
+
# have a nil ID.
|
124
|
+
def self.destroy(*objs)
|
125
|
+
objs = objs.flatten
|
126
|
+
|
127
|
+
resp = request(:delete) do
|
128
|
+
soap.body = {
|
129
|
+
plural_class_name => objs.map { |o| { id: o.id }}
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
Array.wrap(resp[:return][:results]).each_with_index do |result, i|
|
134
|
+
if result[:is_error]
|
135
|
+
objs[i].errors.add(result[:error_code], result[:error_string])
|
136
|
+
else
|
137
|
+
objs[i].id = nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
objs
|
142
|
+
end
|
143
|
+
|
144
|
+
# Accepts a hash whose keys should be setters on the object.
|
145
|
+
def initialize(options = {})
|
146
|
+
self.errors = Errors.new
|
147
|
+
options.each { |k,v| send("#{k}=", v) if respond_to?("#{k}=") }
|
148
|
+
end
|
149
|
+
|
150
|
+
# `to_hash` should be overridden to provide a hash whose structure matches the structure expected by the API.
|
151
|
+
def to_hash
|
152
|
+
{}
|
153
|
+
end
|
154
|
+
|
155
|
+
# Convenience instance method that calls the class `request` method.
|
156
|
+
def request(method, &block)
|
157
|
+
self.class.request(method, &block)
|
158
|
+
end
|
159
|
+
|
160
|
+
def reload
|
161
|
+
return if self.id.blank?
|
162
|
+
|
163
|
+
# The block below is evaluated in a weird scope so we need to capture self as _self for use inside the block.
|
164
|
+
_self = self
|
165
|
+
|
166
|
+
resp = request(:read) do
|
167
|
+
soap.body = { filter: { id: _self.id } }
|
168
|
+
end
|
169
|
+
|
170
|
+
resp[:return].each do |k, v|
|
171
|
+
self.send("#{k}=", v) if self.respond_to? "#{k}="
|
172
|
+
end
|
173
|
+
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# Saves the object. If the object has an ID, it is updated. Otherwise, it is created.
|
178
|
+
def save
|
179
|
+
id.blank? ? create : update
|
180
|
+
end
|
181
|
+
|
182
|
+
# Creates the object. See `Bronto::Base.create` for more info.
|
183
|
+
def create
|
184
|
+
res = self.class.create(self)
|
185
|
+
res.first
|
186
|
+
end
|
187
|
+
|
188
|
+
# Updates the object. See `Bronto::Base.update` for more info.
|
189
|
+
def update
|
190
|
+
self.class.update(self).first
|
191
|
+
end
|
192
|
+
|
193
|
+
# Destroys the object. See `Bronto::Base.destroy` for more info.
|
194
|
+
def destroy
|
195
|
+
self.class.destroy(self).first
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Bronto
|
2
|
+
class Contact < Base
|
3
|
+
attr_accessor :email, :fields, :lists
|
4
|
+
|
5
|
+
# Finds contacts based on the `filter` (Bronto::Filter object).
|
6
|
+
# * `page_number` is the page of contacts to request. Bronto doesn't specify how many contacts are returned per page,
|
7
|
+
# only that you should keep increasing the number until no more contacts are returned.
|
8
|
+
# * `fields` can be an array of field IDs or an array of Field objects.
|
9
|
+
# * `include_lists` determines whether to include the list IDs each contact belongs to.
|
10
|
+
def self.find(filter = Bronto::Filter.new, page_number = 1, fields = nil, include_lists = false)
|
11
|
+
body = { filter: filter.to_hash, page_number: page_number }
|
12
|
+
|
13
|
+
body[:fields] = Array.wrap(fields).map { |f| f.is_a?(Bronto::Field) ? f.id : f } if Array(fields).length > 0
|
14
|
+
body[:include_lists] = include_lists
|
15
|
+
|
16
|
+
resp = request(:read) do
|
17
|
+
soap.body = body
|
18
|
+
end
|
19
|
+
|
20
|
+
Array.wrap(resp[:return]).map { |hash| new(hash) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(options = {})
|
24
|
+
self.fields = {}
|
25
|
+
fields = options.delete(:fields)
|
26
|
+
Array.wrap(fields).each { |field| set_field(field[:field_id], field[:content]) }
|
27
|
+
|
28
|
+
super(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
if id.present?
|
33
|
+
{ id: id, email: email, fields: fields.values.map(&:to_hash) }
|
34
|
+
else
|
35
|
+
{ email: email, fields: fields.values.map(&:to_hash) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_field(field, value)
|
40
|
+
id = field.is_a?(Bronto::Field) ? field.id : field
|
41
|
+
self.fields[id] = Field.new(id, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_field(field)
|
45
|
+
id = field.is_a?(Bronto::Field) ? field.id : field
|
46
|
+
self.fields[id].try(:content)
|
47
|
+
end
|
48
|
+
|
49
|
+
class Field
|
50
|
+
attr_accessor :field_id, :content
|
51
|
+
|
52
|
+
def initialize(id, content)
|
53
|
+
self.field_id = id
|
54
|
+
self.content = content
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_hash
|
58
|
+
{ field_id: field_id, content: content }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|