constant-contact-ruby 0.2.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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ doc
23
+ .yardoc
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 BatchBlue Software, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ = constant-contact-ruby
2
+
3
+ Ruby wrapper for the Constant Contact REST API
4
+
5
+ == Requirements
6
+
7
+ HTTParty gem - http://rubygems.org/gems/httparty
8
+
9
+ == Usage
10
+
11
+ require 'constant-contact-ruby'
12
+ include ConstantContact
13
+
14
+ ### Contacts ###
15
+
16
+ all_contacts = Contact.all
17
+
18
+ contact_7 = Contact.get( 7 )
19
+ contact_7.update_attributes!( :first_name => 'John', :last_name => 'Doe' )
20
+ contact_7.add_to_list!( 3 )
21
+ contact_7.remove_from_list!( 3 )
22
+ contact_7.replace_contact_lists!( 1, 3, 9 )
23
+
24
+ new_contact = Contact.add( :email_address => 'john@example.com', :first_name => 'John', :last_name => 'Doe' )
25
+
26
+ john_doe = Contact.search_by_email( 'john@example.com' )
27
+
28
+
29
+ ### Contact Lists ###
30
+
31
+ all_lists = ContactList.all
32
+
33
+ list_1 = ContactList.get( 1 )
34
+ list_1_members = ContactList.members( list_1.uid )
35
+ list_1.clear_contacts! # remove all contacts from list
36
+ ContactList.delete( 1 ) # delete the list
37
+
38
+ new_list = ContactList.add( 'my new list' )
39
+
40
+
41
+ ### Bulk Activities (involving multiple contacts) ###
42
+
43
+ activities = Activity.all # list all activities in the system
44
+
45
+ activity_details = Activity.get( activities.first.uid )
46
+
47
+ Activity.remove_all_contacts_from_lists( 1, 3, 4 ) # remove all contacts from lists 1, 3, and 4
48
+
49
+ new_contacts = [
50
+ { :email_address = 'user1@example.com', :first_name => 'User1', :last_name => 'test' },
51
+ { :email_address = 'user2@example.com', :first_name => 'User2', :last_name => 'test 2' },
52
+ { :email_address = 'user3@example.com', :first_name => 'User1' },
53
+ { :email_address = 'user4@example.com' }
54
+ ]
55
+ Activity.add_contacts_to_lists( new_contacts, 3, 4 ) # all the users array to lists 3 and 4
56
+ Activity.remove_contacts_from_lists( new_contacts, 4 ) # remove the users from list 4
57
+
58
+
59
+ == Note on Patches/Pull Requests
60
+
61
+ * Fork the project.
62
+ * Make your feature addition or bug fix.
63
+ * Add tests for it. This is important so I don't break it in a
64
+ future version unintentionally.
65
+ * Commit, do not mess with rakefile, version, or history.
66
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
67
+ * Send me a pull request. Bonus points for topic branches.
68
+
69
+ == Authors
70
+
71
+ Craig P Jolicoeur - http://craigjolicoeur.com
72
+
73
+ == Copyright
74
+
75
+ Copyright (c) 2010 BatchBlue Software, LLC. See LICENSE for details.
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "constant-contact-ruby"
8
+ gem.summary = %Q{Constant Contact Ruby API Wrapper}
9
+ gem.description = %Q{Ruby wrapper around the Constant Contact REST API}
10
+ gem.email = "cpjolicoeur@gmail.com"
11
+ gem.homepage = "http://github.com/cpjolicoeur/constant-contact-ruby"
12
+ gem.authors = ["Craig P Jolicoeur"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "constant-contact-ruby #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/TODO ADDED
@@ -0,0 +1 @@
1
+ add export feature to Activities
@@ -0,0 +1,5 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :build:
5
+ :patch: 1
@@ -0,0 +1,63 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{constant-contact-ruby}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Craig P Jolicoeur"]
12
+ s.date = %q{2010-03-03}
13
+ s.description = %q{Ruby wrapper around the Constant Contact REST API}
14
+ s.email = %q{cpjolicoeur@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc",
18
+ "TODO"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ "LICENSE",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "TODO",
27
+ "VERSION.yml",
28
+ "constant-contact-ruby.gemspec",
29
+ "constant-contact-ruby.rb",
30
+ "lib/constant_contact.rb",
31
+ "lib/constant_contact/activity.rb",
32
+ "lib/constant_contact/base_resource.rb",
33
+ "lib/constant_contact/contact.rb",
34
+ "lib/constant_contact/contact_list.rb",
35
+ "test/fixtures/get_contact.xml",
36
+ "test/fixtures/get_contacts.xml",
37
+ "test/fixtures/post_contacts.xml",
38
+ "test/helper.rb",
39
+ "test/test_constant-contact-ruby.rb",
40
+ "test/test_contacts.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/cpjolicoeur/constant-contact-ruby}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.5}
46
+ s.summary = %q{Constant Contact Ruby API Wrapper}
47
+ s.test_files = [
48
+ "test/helper.rb",
49
+ "test/test_constant-contact-ruby.rb",
50
+ "test/test_contacts.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
58
+ else
59
+ end
60
+ else
61
+ end
62
+ end
63
+
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ require 'lib/constant_contact'
5
+ require 'lib/constant_contact/base_resource'
6
+ require 'lib/constant_contact/contact'
7
+ require 'lib/constant_contact/contact_list'
8
+ require 'lib/constant_contact/activity'
@@ -0,0 +1,18 @@
1
+ module ConstantContact
2
+
3
+ include HTTParty
4
+ format :xml
5
+ headers 'Accept' => 'application/atom+xml'
6
+ headers 'Content-Type' => 'application/atom+xml'
7
+
8
+ API_KEY = "59ca4bb4-51e9-4c08-a2b2-a34aac7bb78f"
9
+
10
+ class << self
11
+ # Create a connection to the Constant Contact API using your login credentials
12
+ def setup( user, pass )
13
+ basic_auth "#{API_KEY}%#{user}", pass
14
+ base_uri "https://api.constantcontact.com/ws/customers/#{user.downcase}"
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,196 @@
1
+ module ConstantContact
2
+ class Activity < BaseResource
3
+
4
+ ADD_CONTACTS = 'ADD_CONTACTS'.freeze
5
+ REMOVE_CONTACTS = 'REMOVE_CONTACTS_FROM_LISTS'.freeze
6
+ CLEAR_CONTACTS = 'CLEAR_CONTACTS_FROM_LISTS'.freeze
7
+ EXPORT_CONTACTS = 'EXPORT_CONTACTS'.freeze
8
+
9
+ attr_reader :uid, :original_xml
10
+
11
+ def initialize( params={}, orig_xml='' ) #:nodoc:
12
+ return false if params.empty?
13
+
14
+ @uid = params['id'].split('/').last
15
+ @original_xml = orig_xml
16
+
17
+ fields = params['content']['Activity']
18
+
19
+ if errors = fields.delete( 'Errors' )
20
+ # FIXME: handle the <Errors> node properly
21
+ end
22
+
23
+ fields.each do |k,v|
24
+ underscore_key = underscore( k )
25
+
26
+ instance_eval %{
27
+ @#{underscore_key} = "#{v}"
28
+
29
+ def #{underscore_key}
30
+ @#{underscore_key}
31
+ end
32
+ }
33
+ end
34
+
35
+ end
36
+
37
+ # List all activities
38
+ def self.all( options={} )
39
+ activities = []
40
+
41
+ data = ConstantContact.get( '/activities', options )
42
+ return activities if ( data.nil? or data.empty? or data['feed']['entry'].nil? )
43
+
44
+ data['feed']['entry'].each do |entry|
45
+ activities << new( entry )
46
+ end
47
+
48
+ activities
49
+ end
50
+
51
+ # Get the details of a specific activity
52
+ def self.get( id, options={} )
53
+ activity = ConstantContact.get( "/activities/#{id.to_s}", options )
54
+ return nil if ( activity.nil? or activity.empty? )
55
+ new( activity['entry'], activity.body )
56
+ end
57
+
58
+ # Add multiple users to one or more contact lists
59
+ def self.add_contacts_to_lists( users=[], *lists )
60
+ data_param = build_data_param( users )
61
+ list_param = build_lists_param( *lists )
62
+
63
+ data = ConstantContact.post( '/activities',
64
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
65
+ :body => { 'activityType' => ADD_CONTACTS, :data => data_param, :lists => list_param } )
66
+
67
+ if data.code == 201
68
+ new( data['entry'] )
69
+ else
70
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
71
+ return false
72
+ end
73
+ end
74
+
75
+ # Remove multiple users from a contact list
76
+ def self.remove_contacts_from_lists( users=[], *lists )
77
+ data_param = build_data_param( users )
78
+ list_param = build_lists_param( *lists )
79
+
80
+ data = ConstantContact.post( '/activities',
81
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
82
+ :body => { 'activityType' => REMOVE_CONTACTS, :data => data_param, :lists => list_param } )
83
+
84
+ if data.code == 201
85
+ new( data['entry'] )
86
+ else
87
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
88
+ return false
89
+ end
90
+ end
91
+
92
+ # Remove all users from a specific contact list
93
+ def self.remove_all_contacts_from_lists( *lists )
94
+ list_param = build_lists_param( *lists )
95
+
96
+ data = ConstantContact.post( '/activities',
97
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
98
+ :body => { 'activityType' => CLEAR_CONTACTS, :lists => list_param } )
99
+
100
+ if data.code == 201
101
+ new( data['entry'] )
102
+ else
103
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
104
+ return false
105
+ end
106
+ end
107
+
108
+ # Export subscribers list to a file
109
+ #
110
+ # @param [Integer/String] list_id is the uid of the list to export
111
+ # @param [Array] fields is an array of fields to export for list contacts
112
+ #
113
+ def self.export( list_id, *fields )
114
+ export_columns = build_export_columns( *fields )
115
+
116
+ data = ConstantContact.post( '/activities',
117
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
118
+ :body => {
119
+ 'activityType' => EXPORT_CONTACTS,
120
+ 'fileType' => 'CSV',
121
+ 'exportOptDate' => true,
122
+ 'exportOptSource' => true,
123
+ 'exportListName' => true,
124
+ 'sortBy' => 'EMAIL_ADDRESS',
125
+ 'listId' => ContactList.url_for( list_id ),
126
+ :columns => export_columns
127
+ } )
128
+
129
+ if data.code == 201
130
+ new( data['entry'] )
131
+ else
132
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
133
+ return false
134
+ end
135
+ end
136
+
137
+ private
138
+
139
+ # Build the data= query param for a POST request
140
+ #
141
+ # @param [Array] Users - an array of user hash objects
142
+ #
143
+ def self.build_data_param( users )
144
+ return '' if users.empty?
145
+ data_start, data_end = '', ''
146
+ keys, fields = [], []
147
+
148
+ # get a list of all the key fields and then create values
149
+ users.each do |u|
150
+ u.each_key do |k|
151
+ readable_key = underscore(k).split('_').map{|x| x.capitalize}.join(' ')
152
+ packet = { :original => k, :readable => readable_key }
153
+ keys << packet unless keys.include?( packet )
154
+ end
155
+ end
156
+ data_start = keys.map { |k| k[:readable] }.join(',') + "\n"
157
+
158
+ # now build the data fields
159
+ users.each do |u|
160
+ tmp = ''
161
+ keys.each { |k| tmp << "#{u[k[:original]]}," }
162
+ fields << tmp.chomp(',') + "\n"
163
+ end
164
+ data_end = fields.join
165
+
166
+ return data_start + data_end
167
+ end
168
+
169
+ # Build the lists= param for a POST request
170
+ #
171
+ # @param [Array] lists - an array of list ids
172
+ # @return [String] a usable string for the list param in a POST
173
+ def self.build_lists_param( *lists )
174
+ # list_param = lists.map { |list| ContactList.url_for( list.to_s ) }
175
+ #
176
+ # FIXME: this is hack because passing in an array of lists wasnt working because of Hash.to_params
177
+ # Hash#to_params returns lists[]=foo&lists[]=bar instead of lists=foo&lists=bar
178
+ # I can't even find where Hash#to_params is defined!!!
179
+ list_param = ''
180
+ lists.each do |list|
181
+ list_param << "#{ContactList.url_for( list.to_s )}&lists="
182
+ end
183
+ list_param.chomp('&lists=')
184
+ end
185
+
186
+ def self.build_export_columns( *fields )
187
+ columns_param = ''
188
+ fields.each do |field|
189
+ readable_col = underscore( field ).split('_').map{ |x| x.upcase }.join(' ')
190
+ columns_param << "#{readable_col}&columns="
191
+ end
192
+ columns_param.chomp('&columns=')
193
+ end
194
+
195
+ end # class Activity
196
+ end # module ConstantContact
@@ -0,0 +1,23 @@
1
+ module ConstantContact
2
+ class BaseResource #:nodoc:
3
+
4
+ private
5
+
6
+ def self.camelize( string )
7
+ string.split( /[^a-z0-9]/i ).map{ |w| w.capitalize }.join
8
+ end
9
+
10
+ def camelize( string )
11
+ BaseResource.camelize( string )
12
+ end
13
+
14
+ def self.underscore( string )
15
+ string.to_s.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
16
+ end
17
+
18
+ def underscore( string )
19
+ BaseResource.underscore( string )
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,323 @@
1
+ module ConstantContact
2
+ class Contact < BaseResource
3
+
4
+ attr_reader :uid, :contact_lists, :original_xml
5
+
6
+ def initialize( params={}, orig_xml='', from_contact_list=false ) #:nodoc:
7
+ return false if params.empty?
8
+
9
+ @uid = params['id'].split('/').last
10
+ @original_xml = orig_xml
11
+ @contact_lists = []
12
+
13
+ if from_contact_list
14
+ fields = params['content']['ContactListMember']
15
+ else
16
+ fields = params['content']['Contact']
17
+ end
18
+
19
+ if lists = fields.delete( 'ContactLists' )
20
+ if lists['ContactList'].is_a?( Array )
21
+ @contact_lists = lists['ContactList'].collect { |list| list['id'].split('/').last }
22
+ else
23
+ @contact_lists << lists['ContactList']['id'].split('/').last
24
+ end
25
+ end
26
+
27
+ fields.each do |k,v|
28
+ underscore_key = underscore( k )
29
+
30
+ instance_eval %{
31
+ @#{underscore_key} = "#{v}"
32
+
33
+ def #{underscore_key}
34
+ @#{underscore_key}
35
+ end
36
+ }
37
+ end
38
+
39
+ end # def initialize
40
+
41
+ # Update a single contact record
42
+ #
43
+ # NOTE: you cannot update a Contact's ContactList subscriptions through
44
+ # this method. Use the apropriate ContactList methods instead
45
+ #
46
+ def update_attributes!( params={} )
47
+ return false unless full_record? # TODO: raise some kind of specific error here
48
+
49
+ params.each do |key,val|
50
+ self.instance_variable_set("@#{key.to_s}", val)
51
+ end
52
+
53
+ data = ConstantContact.put( "/contacts/#{self.uid}", :body => self.send(:to_xml) )
54
+ if data.code == 204 # success
55
+ return true
56
+ else
57
+ return false # probably should raise an error here instead
58
+ end
59
+ end
60
+
61
+ # Add user to a contact list
62
+ def add_to_list!( list_id, options={} )
63
+ list_id = list_id.to_s
64
+ xml = update_contact_lists( *(self.contact_lists + [list_id]) )
65
+
66
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
67
+ options.merge!({ :body => xml })
68
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
69
+
70
+ if data.code == 204 # success
71
+ self.contact_lists << list_id unless self.contact_lists.include?( list_id )
72
+ return true
73
+ else
74
+ return false # probably should raise an error here instead
75
+ end
76
+ end
77
+
78
+ # Remove user from a contact list
79
+ def remove_from_list!( list_id, options={} )
80
+ list_id = list_id.to_s
81
+ xml = update_contact_lists( *(self.contact_lists - [list_id]) )
82
+
83
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
84
+ options.merge!({ :body => xml })
85
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
86
+
87
+ if data.code == 204 # success
88
+ self.contact_lists.delete( list_id )
89
+ return true
90
+ else
91
+ return false # probably should raise an error here instead
92
+ end
93
+ end
94
+
95
+ # Set a users contact lists
96
+ def replace_contact_lists!( *lists )
97
+ xml = update_contact_lists( *lists )
98
+
99
+ # FIXME: clean up the following code - it appears in 3 methods in this class!
100
+ options = { :body => xml }
101
+ data = ConstantContact.put( "/contacts/#{self.uid}", options )
102
+
103
+ if data.code == 204 # success
104
+ @contact_lists = lists.map { |l| l.to_s }
105
+ return true
106
+ else
107
+ return false # probably should raise an error here instead
108
+ end
109
+ end
110
+
111
+ # Opt-out from all contact lists
112
+ #
113
+ # Contact will be removed from all lists and become a member of the
114
+ # Do-Not_Mail special list
115
+ def opt_out!( options={} )
116
+ data = ConstantContact.delete( "/contacts/#{self.uid}", options )
117
+
118
+ if data.code == 204
119
+ @contact_lists = []
120
+ return true
121
+ else
122
+ return false
123
+ end
124
+ end
125
+
126
+ # Opt-in a user who has previously opted out
127
+ #--
128
+ # FIXME: this isn't currently working. Currently I keep getting a 403-Forbidden response
129
+ # should this really even be in the API wrapper at all?
130
+ def opt_in!( *lists )
131
+ # # NOTE: same as replace_contact_lists but must set to ACTION_BY_CONTACT
132
+ # xml = update_contact_lists( *lists ).gsub( /<\/ContactLists>/, %Q(<OptInSource>ACTION_BY_CONTACT</OptInSource>\n\t</ContactLists>) )
133
+
134
+ # # FIXME: clean up the following code - it appears in 3 methods in this class!
135
+ # options = { :body => xml }
136
+ # data = ConstantContact.put( "/contacts/#{self.uid}", options )
137
+ #
138
+ # if data.code == 204 # success
139
+ # @contact_lists = lists.map { |l| l.to_s }
140
+ # return true
141
+ # else
142
+ # return false # probably should raise an error here instead
143
+ # end
144
+ end
145
+
146
+ # Get a summary list all contacts
147
+ def self.all( options={} )
148
+ contacts = []
149
+
150
+ data = ConstantContact.get( '/contacts', options )
151
+ return contacts if ( data.nil? or data.empty? )
152
+
153
+ data['feed']['entry'].each do |entry|
154
+ contacts << new( entry )
155
+ end
156
+
157
+ contacts
158
+ end
159
+
160
+ # Add a new contact
161
+ #
162
+ # Required data fields:
163
+ # * EmailAddress => String
164
+ # * ContactLists => Array of list IDs
165
+ #
166
+ # Options data fields:
167
+ # * EmailType
168
+ # * FirstName
169
+ # * MiddleName
170
+ # * LastName
171
+ # * JobTitle
172
+ # * CompanyName
173
+ # * HomePhone
174
+ # * WorkPhone
175
+ # * Addr1
176
+ # * Addr2
177
+ # * Addr3
178
+ # * City
179
+ # * StateCode => Must be valid US/Canada Code (http://ui.constantcontact.com/CCSubscriberAddFileFormat.jsp#states)
180
+ # * StateName
181
+ # * CountryCode = Must be valid code (http://constantcontact.custhelp.com/cgi-bin/constantcontact.cfg/php/enduser/std_adp.php?p_faqid=3614)
182
+ # * CountryName
183
+ # * PostalCode
184
+ # * SubPostalCode
185
+ # * Note
186
+ # * CustomField[1-15]
187
+ # * OptInSource
188
+ # * OptOutSource
189
+ #
190
+ def self.add( data={}, opt_in='ACTION_BY_CUSTOMER', options={} )
191
+ xml = build_contact_xml_packet( data, opt_in )
192
+
193
+ options.merge!({ :body => xml })
194
+ data = ConstantContact.post( "/contacts", options )
195
+
196
+ # check response.code
197
+ if data.code == 201 # Entity Created
198
+ return new( data['entry'] )
199
+ else
200
+ # data.code == 409 # Conflict ( probably a duplicate )
201
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
202
+ return nil
203
+ end
204
+ end
205
+
206
+ # Get detailed record for a single contact by id
207
+ def self.get( id, options={} )
208
+ data = ConstantContact.get( "/contacts/#{id.to_s}", options )
209
+ return nil if ( data.nil? or data.empty? )
210
+ new( data['entry'], data.body )
211
+ end
212
+
213
+ # Search for a contact by last updated date
214
+ #
215
+ # Valid options:
216
+ # * :updated_since => Time object
217
+ # * :list_type => One of 'active'|'removed'|'do-not-mail'
218
+ #
219
+ # def self.search_by_date( options={} )
220
+ # end
221
+
222
+ # Search for a contact by email address
223
+ #
224
+ # @param [String] email => "user@example.com"
225
+ #
226
+ def self.search_by_email( email )
227
+ data = ConstantContact.get( '/contacts', :query => { :email => email.downcase } )
228
+ return [] if ( data.nil? )
229
+
230
+ if data.code == 500
231
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
232
+ return false
233
+ else
234
+ new( data['feed']['entry'] )
235
+ end
236
+ end
237
+
238
+ # Returns the objects API URI
239
+ def self.url_for( id )
240
+ "#{ConstantContact.base_uri}/contacts/#{id}"
241
+ end
242
+
243
+ private
244
+
245
+ def update_contact_lists( *lists )
246
+ str = %Q(<ContactLists>\n)
247
+ lists.each do |list|
248
+ str << %Q( <ContactList id="#{ContactList.url_for( list )}" />\n)
249
+ end
250
+ str << %Q( </ContactLists>)
251
+
252
+ # self.original_xml.gsub(/<ContactLists>.*<\/ContactLists>/m, str)
253
+ if self.original_xml =~ /<ContactLists>.*<\/ContactLists>/m
254
+ self.original_xml.gsub( /#{$&}/, str)
255
+ else
256
+ self.original_xml.gsub( /<\/Contact>/m, "#{str}\n</Contact>" )
257
+ end
258
+ end
259
+
260
+ def self.build_contact_xml_packet( data={}, opt_in='ACTION_BY_CUSTOMER' )
261
+ xml = <<EOF
262
+ <entry xmlns="http://www.w3.org/2005/Atom">
263
+ <title type="text"> </title>
264
+ <updated>#{Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z")}</updated>
265
+ <author> </author>
266
+ <id>data:,none</id>
267
+ <summary type="text">Contact</summary>
268
+ <content type="application/vnd.ctct+xml">
269
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/">
270
+ EOF
271
+
272
+ data.each do |key, val|
273
+ node = camelize(key.to_s)
274
+
275
+ if key == :contact_lists
276
+ xml << %Q( <ContactLists>\n)
277
+ val.each do |list_id|
278
+ xml<< %Q( <ContactList id="#{ContactList.url_for( list_id )}" />\n)
279
+ end
280
+ xml << %Q( </ContactLists>\n)
281
+ else
282
+ xml << %Q( <#{node}>#{val}</#{node}>\n)
283
+ end
284
+ end
285
+
286
+ xml += <<EOF
287
+ <OptInSource>#{opt_in}</OptInSource>
288
+ </Contact>
289
+ </content>
290
+ </entry>
291
+ EOF
292
+ xml
293
+ end # def build_contact_xml_packet
294
+
295
+ # Is this a full contact record?
296
+ def full_record?
297
+ !self.contact_lists.empty?
298
+ end
299
+
300
+ # convert a full Contact record into XML format
301
+ def to_xml
302
+ return nil unless full_record?
303
+
304
+ do_not_process = [ "@contact_lists", "@original_source", "@original_xml", "@uid", "@xmlns" ]
305
+
306
+ xml = self.original_xml
307
+
308
+ self.instance_variables.each do |ivar|
309
+ next if do_not_process.include?( ivar )
310
+
311
+ var = camelize( ivar.gsub(/@/,'') )
312
+
313
+ xml.gsub!( /<#{var}>(.*)<\/#{var}>/ , "<#{var}>#{self.instance_variable_get(ivar)}</#{var}>" )
314
+ end
315
+
316
+ # replace <updated> node with current time
317
+ xml.gsub( /<updated>.*<\/updated>/, Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z") )
318
+
319
+ xml
320
+ end
321
+
322
+ end # class Contact
323
+ end # module ConstantContact
@@ -0,0 +1,152 @@
1
+ module ConstantContact
2
+ class ContactList < BaseResource
3
+
4
+ attr_reader :uid, :original_xml
5
+
6
+ def initialize( params={}, orig_xml='' ) #:nodoc:
7
+ return false if params.empty?
8
+
9
+ @uid = params['id'].split('/').last
10
+ @original_xml = orig_xml
11
+
12
+ params['content']['ContactList'].each do |k,v|
13
+ underscore_key = underscore( k )
14
+
15
+ instance_eval %{
16
+ @#{underscore_key} = "#{v}"
17
+
18
+ def #{underscore_key}
19
+ @#{underscore_key}
20
+ end
21
+ }
22
+ end
23
+ end
24
+
25
+ # Update a contacts attributes
26
+ def update_attributes!( params={} )
27
+ return false unless full_record?
28
+
29
+ params.each do |key,val|
30
+ self.instance_variable_set("@#{key.to_s}", val)
31
+ end
32
+
33
+ data = ConstantContact.put( "/lists/#{self.uid}", :body => self.send(:to_xml) )
34
+
35
+ if data.code == 204
36
+ return true
37
+ else
38
+ return data # probably should raise an error here instead
39
+ end
40
+ end
41
+
42
+ # Remove all contacts from the contact list
43
+ def clear_contacts!
44
+ Activity.remove_all_contacts_from_lists( self.uid )
45
+ end
46
+
47
+ # Get all contact lists
48
+ def self.all( options={} )
49
+ lists = []
50
+
51
+ data = ConstantContact.get( '/lists', options )
52
+ return lists if ( data.nil? or data.empty? )
53
+
54
+ data['feed']['entry'].each do |entry|
55
+ lists << new( entry )
56
+ end
57
+
58
+ lists
59
+ end
60
+
61
+ # Add a new contact list
62
+ def self.add( name, opt_in=false, sort_order=99, options={} )
63
+ return nil if( name.nil? || name.empty? )
64
+
65
+ xml = build_contact_list_xml_packet( name, opt_in, sort_order )
66
+
67
+ options.merge!({ :body => xml })
68
+ data = ConstantContact.post( "/lists", options )
69
+
70
+ if data.code == 201
71
+ return new( data['entry'] )
72
+ else
73
+ puts "HTTP Status Code: #{data.code}, message: #{data.message}"
74
+ return nil
75
+ end
76
+ end
77
+
78
+ # Delete a contact list
79
+ def self.delete( id, options={} )
80
+ data = ConstantContact.delete( "/lists/#{id.to_s}", options )
81
+ return ( data.code == 204 ) ? true : false
82
+ end
83
+
84
+ # Get a single contact list
85
+ def self.get( id, options={} )
86
+ list = ConstantContact.get( "/lists/#{id.to_s}", options )
87
+ return nil if ( list.nil? or list.empty? )
88
+ new( list['entry'], list.body )
89
+ end
90
+
91
+ # Get a lists members
92
+ def self.members( id, options={} )
93
+ members = ConstantContact.get( "/lists/#{id.to_s}/members", options )
94
+ return nil if ( members.nil? or members.empty? )
95
+ members['feed']['entry'].collect { |entry| Contact.new( entry, '', true ) }
96
+ end
97
+
98
+ # Returns the objects API URI
99
+ def self.url_for( id )
100
+ "#{ConstantContact.base_uri}/lists/#{id}"
101
+ end
102
+
103
+ private
104
+
105
+ def self.build_contact_list_xml_packet( name, opt_in=false, sort=99 )
106
+ xml = <<EOF
107
+ <entry xmlns="http://www.w3.org/2005/Atom">
108
+ <updated>#{Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z")}</updated>
109
+ <title />
110
+ <author />
111
+ <id>data:,none</id>
112
+ <content type="application/vnd.ctct+xml">
113
+ <ContactList xmlns="http://ws.constantcontact.com/ns/1.0/">
114
+ <Name>#{name}</Name>
115
+ <SortOrder>#{sort}</SortOrder>
116
+ <OptInDefault>#{opt_in.to_s}</OptInDefault>
117
+ </ContactList>
118
+ </content>
119
+ </entry>
120
+ EOF
121
+ xml
122
+ end
123
+
124
+ # Is this a full record or a summary record?
125
+ def full_record?
126
+ !self.members.emtpy?
127
+ end
128
+
129
+ # Convert the object into the needed API XML format
130
+ def to_xml
131
+ return nil unless full_record?
132
+
133
+ do_not_process = [ "@original_xml", "@uid", "@members" ]
134
+
135
+ xml = self.original_xml
136
+
137
+ self.instance_variables.each do |ivar|
138
+ next if do_not_process.include?( ivar )
139
+
140
+ var = camelize( ivar.gsub(/@/,'') )
141
+
142
+ xml.gsub!( /<#{var}>(.*)<\/#{var}>/ , "<#{var}>#{self.instance_variable_get(ivar)}</#{var}>" )
143
+ end
144
+
145
+ # replace <updated> node with current time
146
+ xml.gsub( /<updated>.*<\/updated>/, Time.now.strftime("%Y-%m-%dT%H:%M:%S.000Z") )
147
+
148
+ xml
149
+ end
150
+
151
+ end # class ContactList
152
+ end # module ConstantContact
@@ -0,0 +1,72 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <entry xmlns="http://www.w3.org/2005/Atom">
3
+ <link href="/ws/customers/joesflowers/contacts/22199" rel="edit" />
4
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/22199</id>
5
+ <title type="text">Contact: joe@example.com</title>
6
+ <updated>2009-11-20T20:41:19.243Z</updated>
7
+ <author>
8
+ <name>Constant Contact</name>
9
+ </author>
10
+ <content type="application/vnd.ctct+xml">
11
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
12
+ com/ws/customers/joesflowers/contacts/22199">
13
+ <Status>Active</Status>
14
+ <EmailAddress>joe@example.com</EmailAddress>
15
+ <EmailType>HTML</EmailType>
16
+ <Name>Customer Joe</Name>
17
+ <FirstName>Joe</FirstName>
18
+ <MiddleName></MiddleName>
19
+ <LastName>Smith</LastName>
20
+ <JobTitle></JobTitle>
21
+ <CompanyName></CompanyName>
22
+ <HomePhone></HomePhone>
23
+ <WorkPhone></WorkPhone>
24
+ <Addr1></Addr1>
25
+ <Addr2></Addr2>
26
+ <Addr3></Addr3>
27
+ <City></City>
28
+ <StateCode>MA</StateCode>
29
+ <StateName>Massachusetts</StateName>
30
+ <CountryCode>us</CountryCode>
31
+ <CountryName>United States</CountryName>
32
+ <PostalCode>02154</PostalCode>
33
+ <SubPostalCode>1781</SubPostalCode>
34
+ <Note></Note>
35
+ <CustomField1></CustomField1>
36
+ <CustomField2></CustomField2>
37
+ <CustomField3></CustomField3>
38
+ <CustomField4></CustomField4>
39
+ <CustomField5></CustomField5>
40
+ <CustomField6></CustomField6>
41
+ <CustomField7></CustomField7>
42
+ <CustomField8></CustomField8>
43
+ <CustomField9></CustomField9>
44
+ <CustomField10></CustomField10>
45
+ <CustomField11></CustomField11>
46
+ <CustomField12></CustomField12>
47
+ <CustomField13></CustomField13>
48
+ <CustomField14></CustomField14>
49
+ <CustomField15></CustomField15>
50
+ <ContactLists>
51
+ <ContactList id="http://api.constantcontact.com/ws/customers/joesflowers/lists/3">
52
+ <link xmlns="http://www.w3.org/2005/Atom" href="/ws/customers/joesflowers/lists/3" rel="self" />
53
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
54
+ <OptInTime>2009-11-20T20:41:06.595Z</OptInTime>
55
+ </ContactList>
56
+ </ContactLists>
57
+ <Confirmed>false</Confirmed>
58
+ <InsertTime>2009-11-20T20:41:06.593Z</InsertTime>
59
+ <LastUpdateTime>2009-11-20T20:41:19.243Z</LastUpdateTime>
60
+ </Contact>
61
+ </content>
62
+ <source>
63
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts</id>
64
+ <title type="text">Contacts for Customer: joesflowers</title>
65
+ <link href="contacts" />
66
+ <link href="contacts" rel="self" />
67
+ <author>
68
+ <name>joesflowers</name>
69
+ </author>
70
+ <updated>2009-11-20T20:45:44.199Z</updated>
71
+ </source>
72
+ </entry>
@@ -0,0 +1,54 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <feed xmlns="http://www.w3.org/2005/Atom">
3
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts</id>
4
+ <title type="text">Contacts for Customer: joesflowers</title>
5
+ <link href="contacts" />
6
+ <link href="contacts" rel="self" />
7
+ <author>
8
+ <name>joesflowers</name>
9
+ </author>
10
+ <updated>2009-11-19T14:49:21.928Z</updated>
11
+ <link href="/ws/customers/joesflowers/contacts?next=g24umerp-fymhqg" rel="next" />
12
+ <link href="/ws/customers/joesflowers/contacts" rel="first" />
13
+ <link href="/ws/customers/joesflowers/contacts" rel="current" />
14
+ <entry>
15
+ <link href="/ws/customers/joesflowers/contacts/21930" rel="edit" />
16
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/21930</id>
17
+ <title type="text">Contact: 1258642119407@example.com</title>
18
+ <updated>2009-11-19T14:49:23.203Z</updated>
19
+ <author>
20
+ <name>Constant Contact</name>
21
+ </author>
22
+ <content type="application/vnd.ctct+xml">
23
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
24
+ com/ws/customers/joesflowers/contacts/21930">
25
+ <Status>Active</Status>
26
+ <EmailAddress>1258642119407@example.com</EmailAddress>
27
+ <EmailType>HTML</EmailType>
28
+ <Name>Customer 1</Name>
29
+ <OptInTime>2009-11-19T14:48:41.761Z</OptInTime>
30
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
31
+ </Contact>
32
+ </content>
33
+ </entry>
34
+ <entry>
35
+ <link href="/ws/customers/joesflowers/contacts/21929" rel="edit" />
36
+ <id>http://api.constantcontact.com/ws/customers/joesflowers/contacts/21929</id>
37
+ <title type="text">Contact: 1258641848349@example.com</title>
38
+ <updated>2009-11-19T14:49:23.204Z</updated>
39
+ <author>
40
+ <name>Constant Contact</name>
41
+ </author>
42
+ <content type="application/vnd.ctct+xml">
43
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/" id="http://api.constantcontact.
44
+ com/ws/customers/joesflowers/contacts/21929">
45
+ <Status>Active</Status>
46
+ <EmailAddress>1258641848349@example.com</EmailAddress>
47
+ <EmailType>HTML</EmailType>
48
+ <Name>Customer 2</Name>
49
+ <OptInTime>2009-11-19T14:44:10.873Z</OptInTime>
50
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
51
+ </Contact>
52
+ </content>
53
+ </entry>
54
+ </feed>
@@ -0,0 +1,18 @@
1
+ <entry xmlns="http://www.w3.org/2005/Atom">
2
+ <title type="text"> </title>
3
+ <updated>2008-07-23T14:21:06.407Z</updated>
4
+ <author></author>
5
+ <id>data:,none</id>
6
+ <summary type="text">Contact</summary>
7
+ <content type="application/vnd.ctct+xml">
8
+ <Contact xmlns="http://ws.constantcontact.com/ns/1.0/">
9
+ <EmailAddress>test_100@example.com</EmailAddress>
10
+ <FirstName>First</FirstName>
11
+ <LastName>Last</LastName>
12
+ <OptInSource>ACTION_BY_CONTACT</OptInSource>
13
+ <ContactLists>
14
+ <ContactList id="http://api.constantcontact.com/ws/customers/geeksquad/joesflowers/lists/1" />
15
+ </ContactLists>
16
+ </Contact>
17
+ </content>
18
+ </entry>
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'fakeweb'
4
+
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib', 'constant_contact'))
8
+ require 'constant-contact-ruby'
9
+
10
+ class Test::Unit::TestCase
11
+
12
+ fixtures_path = File.join( File.dirname( __FILE__ ), 'fixtures' )
13
+
14
+ # setup FakeWeb
15
+ FakeWeb.allow_net_connect = false
16
+ # GET /contacts
17
+ FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contacts$}, :body => File.read( File.join( fixtures_path, 'get_contacts.xml' ) ) )
18
+ # GET /contact/:id
19
+ FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contact/\d+$}, :body => File.read( File.join( fixtures_path, 'get_contact.xml' ) ) )
20
+
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'helper'
2
+
3
+ class TestConstantContactRuby < Test::Unit::TestCase
4
+ def test_setup
5
+ ConstantContact.setup( 'u', 'p' )
6
+ assert_equal 'https://api.constantcontact.com/ws/customers/u', ConstantContact.base_uri
7
+ end
8
+ end
@@ -0,0 +1,49 @@
1
+ require 'helper'
2
+ require 'contact'
3
+
4
+ class TestContact < Test::Unit::TestCase
5
+
6
+ def setup
7
+ ConstantContact.setup( 'user', 'password' )
8
+ end
9
+
10
+ def test_all_contacts
11
+ contacts = ConstantContact::Contact.all
12
+ assert !contacts.nil?
13
+ assert_equal 2, contacts.size
14
+ assert_equal ConstantContact::Contact, contacts.first.class
15
+
16
+ assert_equal 'Customer 1', contacts.first.name
17
+ assert_equal '21930', contacts.first.uid
18
+ end
19
+
20
+ def test_all_contacts_with_bad_credentials
21
+ # FakeWeb.register_uri( :get, %r{https://.+:.+@api\.constantcontact\.com/ws/customers/.+/contacts}, :body => '', :status => ['403', 'Not Authorized'] )
22
+ # contacts = ConstantContact::Contact.all
23
+ # assert contacts.empty?
24
+ end
25
+
26
+ def test_get_contact
27
+ contact = ConstantContact::Contact.get( 22199 )
28
+ assert_equal 'Customer Joe', contact.name
29
+ assert_equal '22199', contact.uid
30
+ assert_equal 'joe@example.com', contact.emailaddress
31
+
32
+ # TODO: contactLists
33
+ end
34
+
35
+ def test_add_contact
36
+ # c = ConstantContact::Contact.add(
37
+ # :email_address => 'test@example.com',
38
+ # :first_name => 'First',
39
+ # :last_name => 'Name',
40
+ # :opt_in_source => 'ACTION_BY_CONTACT', # or ACTION_BY_CUSTOMER
41
+ # :contact_lists => [1]
42
+ # )
43
+
44
+ # assert_equal 'test@example.com', c.email_address
45
+ #
46
+ # contact = ConstantContact::Contact.get( c.uid )
47
+ # assert_equal 'test@example.com', contact.email_address
48
+ end
49
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constant-contact-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Craig P Jolicoeur
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-03 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Ruby wrapper around the Constant Contact REST API
17
+ email: cpjolicoeur@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ - TODO
26
+ files:
27
+ - .document
28
+ - .gitignore
29
+ - LICENSE
30
+ - README.rdoc
31
+ - Rakefile
32
+ - TODO
33
+ - VERSION.yml
34
+ - constant-contact-ruby.gemspec
35
+ - constant-contact-ruby.rb
36
+ - lib/constant_contact.rb
37
+ - lib/constant_contact/activity.rb
38
+ - lib/constant_contact/base_resource.rb
39
+ - lib/constant_contact/contact.rb
40
+ - lib/constant_contact/contact_list.rb
41
+ - test/fixtures/get_contact.xml
42
+ - test/fixtures/get_contacts.xml
43
+ - test/fixtures/post_contacts.xml
44
+ - test/helper.rb
45
+ - test/test_constant-contact-ruby.rb
46
+ - test/test_contacts.rb
47
+ has_rdoc: true
48
+ homepage: http://github.com/cpjolicoeur/constant-contact-ruby
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --charset=UTF-8
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.3.5
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Constant Contact Ruby API Wrapper
75
+ test_files:
76
+ - test/helper.rb
77
+ - test/test_constant-contact-ruby.rb
78
+ - test/test_contacts.rb