eloqua 1.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.
Files changed (72) hide show
  1. data/.gitignore +11 -0
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +23 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE +21 -0
  7. data/README.md +245 -0
  8. data/Rakefile +13 -0
  9. data/TODO.md +8 -0
  10. data/eloqua.gemspec +32 -0
  11. data/eloqua_initializer.tpl.rb +3 -0
  12. data/lib/eloqua.rb +51 -0
  13. data/lib/eloqua/api.rb +119 -0
  14. data/lib/eloqua/api/action.rb +41 -0
  15. data/lib/eloqua/api/service.rb +240 -0
  16. data/lib/eloqua/asset.rb +31 -0
  17. data/lib/eloqua/builder/templates.rb +31 -0
  18. data/lib/eloqua/builder/xml.rb +129 -0
  19. data/lib/eloqua/entity.rb +72 -0
  20. data/lib/eloqua/exceptions.rb +5 -0
  21. data/lib/eloqua/helper/attribute_map.rb +78 -0
  22. data/lib/eloqua/query.rb +291 -0
  23. data/lib/eloqua/remote_object.rb +274 -0
  24. data/lib/eloqua/version.rb +3 -0
  25. data/lib/eloqua/wsdl/action.wsdl +1 -0
  26. data/lib/eloqua/wsdl/data.wsdl +1 -0
  27. data/lib/eloqua/wsdl/email.wsdl +1 -0
  28. data/lib/eloqua/wsdl/service.wsdl +1 -0
  29. data/lib/tasks/test.rake +24 -0
  30. data/rspec.watchr +74 -0
  31. data/spec/fixtures/add_group_member/success.xml +18 -0
  32. data/spec/fixtures/create/contact_duplicate.xml +30 -0
  33. data/spec/fixtures/create/contact_success.xml +25 -0
  34. data/spec/fixtures/create_asset/failure.xml +30 -0
  35. data/spec/fixtures/create_asset/group_success.xml +25 -0
  36. data/spec/fixtures/delete_asset/access_deny.xml +31 -0
  37. data/spec/fixtures/describe_asset/success.xml +72 -0
  38. data/spec/fixtures/describe_asset_type/success.xml +23 -0
  39. data/spec/fixtures/describe_entity/success.xml +54 -0
  40. data/spec/fixtures/describe_entity_type/success.xml +45 -0
  41. data/spec/fixtures/get_member_count_in_step_by_status/success.xml +15 -0
  42. data/spec/fixtures/list_asset_types/success.xml +28 -0
  43. data/spec/fixtures/list_entity_types/success.xml +21 -0
  44. data/spec/fixtures/list_group_membership/success.xml +25 -0
  45. data/spec/fixtures/list_members_in_step_by_status/success.xml +15 -0
  46. data/spec/fixtures/query/contact_email_one.xml +38 -0
  47. data/spec/fixtures/query/contact_email_two.xml +56 -0
  48. data/spec/fixtures/query/contact_missing.xml +19 -0
  49. data/spec/fixtures/query/fault.xml +43 -0
  50. data/spec/fixtures/remove_group_member/success.xml +18 -0
  51. data/spec/fixtures/retrieve/contact_missing.xml +17 -0
  52. data/spec/fixtures/retrieve/contact_multiple.xml +3460 -0
  53. data/spec/fixtures/retrieve/contact_single.xml +38 -0
  54. data/spec/fixtures/retrieve_asset/failure.xml +17 -0
  55. data/spec/fixtures/retrieve_asset/success.xml +50 -0
  56. data/spec/fixtures/update/contact_success.xml +26 -0
  57. data/spec/lib/eloqua/api/action_spec.rb +36 -0
  58. data/spec/lib/eloqua/api/service_spec.rb +498 -0
  59. data/spec/lib/eloqua/api_spec.rb +133 -0
  60. data/spec/lib/eloqua/asset_spec.rb +63 -0
  61. data/spec/lib/eloqua/builder/templates_spec.rb +68 -0
  62. data/spec/lib/eloqua/builder/xml_spec.rb +254 -0
  63. data/spec/lib/eloqua/entity_spec.rb +224 -0
  64. data/spec/lib/eloqua/helper/attribute_map_spec.rb +14 -0
  65. data/spec/lib/eloqua/query_spec.rb +596 -0
  66. data/spec/lib/eloqua/remote_object_spec.rb +742 -0
  67. data/spec/lib/eloqua_spec.rb +171 -0
  68. data/spec/shared/attribute_map.rb +173 -0
  69. data/spec/shared/class_to_api_delegation.rb +50 -0
  70. data/spec/spec_helper.rb +48 -0
  71. data/spec/support/helper.rb +73 -0
  72. metadata +366 -0
@@ -0,0 +1,11 @@
1
+ eloqua_initializer.rb
2
+ .DS_Store
3
+ irb.rb
4
+ .bundle
5
+ .idea
6
+ doc
7
+ .yardoc
8
+ test.rb
9
+ spec/reports
10
+ *.gem
11
+ .rvmrc
@@ -0,0 +1,2 @@
1
+ --title "Eloqua API"
2
+ -m markdown
@@ -0,0 +1,23 @@
1
+ ## 0.2.3
2
+ - Added last_request, last_response to Eloqua::API
3
+
4
+ - Added wrapper around the Savon SOAP and HTTP exceptions.
5
+ Savon should disable its error handling and let Eloqua raise its own
6
+ errors by using `Savon::configure {|config| config.raise_errors =
7
+ false }`. This allows us to capture last_request and last_response
8
+ even when there is a Soap or HTTP fault
9
+
10
+ ## 0.2
11
+ - Abstracted out majority of requests into Eloqua::Api::Service
12
+
13
+ - Added Eloqua::Api::Service for a lightweight interface to Eloqua used
14
+ by other components like RemoteObject, Entity and Asset
15
+
16
+ - Eloqua::Query advanced Query builder with support for paging through
17
+ the entire set of records spanning multiple requests
18
+
19
+
20
+ ## 0.1
21
+ - Initial Release
22
+
23
+ - Support for Service Api
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,81 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ eloqua (1.1.0)
5
+ activemodel
6
+ activesupport (= 3.0.6)
7
+ builder
8
+ i18n (= 0.5.0)
9
+ savon
10
+
11
+ GEM
12
+ remote: http://rubygems.org/
13
+ specs:
14
+ activemodel (3.0.6)
15
+ activesupport (= 3.0.6)
16
+ builder (~> 2.1.2)
17
+ i18n (~> 0.5.0)
18
+ activesupport (3.0.6)
19
+ archive-tar-minitar (0.5.2)
20
+ builder (2.1.2)
21
+ ci_reporter (1.6.4)
22
+ builder (>= 2.1.2)
23
+ columnize (0.3.6)
24
+ crack (0.1.8)
25
+ diff-lcs (1.1.2)
26
+ flexmock (0.9.0)
27
+ gyoku (0.4.2)
28
+ builder (>= 2.1.2)
29
+ httpi (0.9.2)
30
+ ntlm-http (>= 0.1.1)
31
+ rack
32
+ i18n (0.5.0)
33
+ linecache19 (0.5.12)
34
+ ruby_core_source (>= 0.1.4)
35
+ mocha (0.9.12)
36
+ ntlm-http (0.1.1)
37
+ rack (1.2.2)
38
+ rspec (2.5.0)
39
+ rspec-core (~> 2.5.0)
40
+ rspec-expectations (~> 2.5.0)
41
+ rspec-mocks (~> 2.5.0)
42
+ rspec-core (2.5.1)
43
+ rspec-expectations (2.5.0)
44
+ diff-lcs (~> 1.1.2)
45
+ rspec-mocks (2.5.0)
46
+ ruby-debug-base19 (0.11.25)
47
+ columnize (>= 0.3.1)
48
+ linecache19 (>= 0.5.11)
49
+ ruby_core_source (>= 0.1.4)
50
+ ruby-debug19 (0.11.6)
51
+ columnize (>= 0.3.1)
52
+ linecache19 (>= 0.5.11)
53
+ ruby-debug-base19 (>= 0.11.19)
54
+ ruby-fsevent (0.2.1)
55
+ ruby_core_source (0.1.5)
56
+ archive-tar-minitar (>= 0.5.2)
57
+ savon (0.8.6)
58
+ builder (>= 2.1.2)
59
+ crack (~> 0.1.8)
60
+ gyoku (>= 0.3.0)
61
+ httpi (>= 0.7.8)
62
+ savon_spec (0.1.4)
63
+ mocha (>= 0.9.8)
64
+ rspec (>= 2.0.0)
65
+ savon (~> 0.8.0)
66
+ timecop (0.3.5)
67
+ watchr (0.7)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ ci_reporter
74
+ eloqua!
75
+ flexmock
76
+ rspec
77
+ ruby-debug19
78
+ ruby-fsevent
79
+ savon_spec
80
+ timecop
81
+ watchr
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011 Sahaja James Lal
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.
21
+
@@ -0,0 +1,245 @@
1
+ # Eloqua API for Ruby
2
+
3
+ Currently supports the majority of the ServiceAPI for Eloqua.
4
+
5
+ The Service API supports the CURD of Entities (Contacts, etc..) and Assets (ContactGroups)
6
+
7
+ Through {Eloqua::Query} supports advanced queries spanning multiple
8
+ requests.
9
+
10
+ ## At a low level
11
+
12
+ For low level requests we offer the {Eloqua::Api::Service} (other Api's
13
+ soon) with the "api" you can make calls like this:
14
+
15
+ Eloqua::Api::Service.describe_type(:asset, 'ContactGroup')
16
+
17
+ The majority of the functions in the api require a "group" which
18
+ is either :entity or :asset.
19
+
20
+ The other common object you will need to have on hand is the "type"
21
+ both entities and assets in Eloqua have types. Types look like this:
22
+
23
+ {
24
+ :id => 1,
25
+ :type => 'Base',
26
+ :name => 'Contact'
27
+ }
28
+
29
+ The most important thing is the :type. See
30
+ {Eloqua::Api::Service.describe} and {Eloqua::Api::Service.describe_type}
31
+ eloqua provides describe and describe_type for gathering information of
32
+ types the what fields those types provide. Types are similar to SQL
33
+ tables.
34
+
35
+ Also see {Eloqua::Api.remote_type} a helper method for generating the
36
+ hash above.
37
+
38
+ Here is an example of a find request (which requires a group and type)
39
+
40
+ group = :entity
41
+ type = Eloqua::Api.remote_type('Contact')
42
+ # => {:id => 1, :type => 'Base', :name => 'Contact'}
43
+
44
+ # Executes a Retreive SOAP call
45
+ record = Eloqua::Api::Service.find_object(
46
+ group,
47
+ type,
48
+ 1 # object id
49
+ )
50
+
51
+ record
52
+ # Keys are internal_name of fields
53
+ # => {:id => 1, :C_EmailAddress => 'email@address.com', ...}
54
+
55
+ Through the low level api we offer the following Eloqua SOAP methods
56
+
57
+
58
+ ### Supported low level requests
59
+
60
+ - CURD (+ Find)
61
+ - Retrieve[Asset] => {Eloqua::Api::Service.find_object}
62
+ - Update[Asset] => {Eloqua::Api::Service.update_object}
63
+ - Create[Asset] => {Eloqua::Api::Service.create_object}
64
+ - Delete[Asset] => {Eloqua::Api::Service.delete_object}
65
+
66
+ - Describing Fields and Types
67
+ - Describe[Asset|Entity]Type => {Eloqua::Api::Service.describe_type}
68
+ - Describe[Asset|Entity] => {Eloqua::Api::Service.describe}
69
+ - List[Asset|Entity]Types => {Eloqua::Api::Service.list_types}
70
+
71
+ - Memberships (Contact Groups)
72
+ - AddGroupMember => {Eloqua::Api::Service.add_group_member}
73
+ - RemoveGroupMember => {Eloqua::Api::Service.remove_group_member}
74
+ - ListGroupMembership => {Eloqua::Api::Service.list_memberships}
75
+
76
+ ## At a high level (Models)
77
+
78
+ Through {Eloqua::Entity} and {Eloqua::Asset} we offer base classes
79
+ for modeling both entities and assets.
80
+
81
+ Both inherit from {Eloqua::RemoteObject} which implements a number of
82
+ ActiveModel features (persistance, dirty attributes, validations mass
83
+ assignment security)
84
+
85
+ To create a model (Sorry, no generator yet!) its as simple as inheriting
86
+ from entity or asset and then specifying a type.
87
+
88
+ class Contact < Eloqua::Entity
89
+ self.remote_type = api.remote_type('Contact')
90
+ end
91
+
92
+ With just this you have instant access to the data with familiar
93
+ {Eloqua::RemoteObject#save save}, {Eloqua::RemoteObject#update_attributes update attributes}, {Eloqua::RemoteObject#persisted? persisted?}, {Eloqua::RemoteObject etc}
94
+
95
+ Magic accessor are also created for "map(ped)" attributes or objects
96
+ that where retreived remotely. See below
97
+
98
+ ### Attribute Mapping
99
+
100
+
101
+ First you should note that C\_ (thats `/^C\_/`) is replaced from
102
+ all attribute names and they are underscored (`.underscore`) for instance:
103
+
104
+ eloqua_does_this = 'C_EmailAddress'
105
+ you_do_this = your_model.email_addres
106
+
107
+ Because of the naming schema for Eloqua "internal_name" there where many
108
+ times where I felt I would rather use a different name that was easier
109
+ to type and remember. With this in mind I created attribute mapping.
110
+
111
+ Here we map C_EmailAddress to email
112
+
113
+ class Contact < Eloqua::Entity
114
+ self.remote_type = api.remote_type('Contact')
115
+ # use the FULL original name including C_ and CamelCase
116
+ map :C_EmailAddress => :email
117
+ end
118
+
119
+ Now we can reference our contacts email with `.email`
120
+
121
+ ### Saving your data
122
+
123
+ When you retrieve object from Eloqua through {Eloqua::RemoteObject#find
124
+ find} or through {Eloqua::Query} that object will be aware of all of its
125
+ attributes (or the ones selected in the query) and will map them back to
126
+ Eloqua's original `internal_name` scheme during the save.
127
+
128
+ When you create a new object however you need define those fields through map.
129
+
130
+ class Contact < Eloqua::Entity
131
+ self.remote_type = api.remote_type('Contact')
132
+ end
133
+
134
+ record = Contact.new
135
+ record.email_address = 'new@email.com' # ERROR
136
+
137
+ class Contact < Eloqua::Entity
138
+ self.remote_type = api.remote_type('Contact')
139
+ map :C_EmailAddress => :email_address
140
+ end
141
+
142
+ record = Contact.new
143
+ record.email= 'new@email.com'
144
+ record.save # SUCCESS
145
+
146
+ # This will successfuly map .email => C_EmailAddress
147
+
148
+ ### What about class methods?
149
+
150
+ Models support all functionality provided in {Eloqua::Api::Service}
151
+ through {Eloqua.delegate_with_args}.
152
+
153
+ Where a group is argument is needed by the low level api the
154
+ model will provide it with its group (entity or asset).
155
+ Where a type is needed the model will provide the models
156
+ {Eloqua::RemoteObject.remote_type remote_type}
157
+
158
+
159
+ # delegates to Eloqua::Api::Service.describe(:entity, Contact.remote_type)
160
+ Contact.describe
161
+
162
+ # delegates to Eloqua::Api::Service.describe_type(:entity, 'Contact')
163
+ # Notice that the second argument is now the first and is required
164
+ Contact.describe_type('Contact')
165
+
166
+
167
+ ## Queries
168
+
169
+ Eloqua provides a method for accessing your data.
170
+
171
+ There are a few important things you need to know about this first.
172
+
173
+ 1. You may only query 200 records at once. You may pull in more via
174
+ pages in a seperate request (pagination)
175
+
176
+ 2. You may only make a Query request once per second.
177
+ (Concurency is a no-go in some situations)
178
+
179
+ 3. I would highly recommend limiting the returned rows.
180
+ The limit on other requests is very high so you can make many more
181
+ find/update/create/delete, etc.. requests then you can queries.
182
+
183
+ I would reccomend gathering EloquaIDs through query and then manipulating
184
+ data through those EloquaIDs in other operations
185
+
186
+ Through {Eloqua::Query} you can search through your Eloqua database.
187
+
188
+ Given we have this Contact class:
189
+
190
+
191
+ class Contact < Eloqua::Contact
192
+ self.remote_type = api.remote_type('Contact')
193
+
194
+ map :C_EmailAddress => :email
195
+ map :C_DateCreated => :created_at
196
+ map :C_DateModified => :updated_at
197
+
198
+ end
199
+
200
+ We can then search for all email addresses in the lightsofapollo.com
201
+ domain.
202
+
203
+ # Entity.where is an alias for Eloqua::Query.new(Contact)
204
+ query = Contact.where
205
+ query.on(:email, '=', '*@lightsofapollo.com') # * is a wildcard
206
+ query.all # makes request returns Array
207
+
208
+ Or all contacts created today
209
+
210
+ query.clear_conditions! # resets request
211
+ query.on(:created_at, '>', Time.now.strftime('%Y-%m-%d'))
212
+ query.each do |record| # this will also make request and iterator through results
213
+ ...
214
+ end
215
+
216
+ Or something more complex
217
+
218
+ query.clear_conditions!
219
+ query.on(:email, '=', '*@lightsofapollo.com').\ # email search
220
+ on(:updated_at, '>', '2011-01-01').\ # updated at >
221
+ limit(1).\ # we only want one record
222
+ fields([:email, 'ContactID']) # only return a record with the email and id fields populated
223
+
224
+ query.all
225
+
226
+ As you might have guessed query will return an Array of Objects of the
227
+ type given.
228
+
229
+ # Will return ContactGroup.new(s)
230
+ Eloqua::Query.new(ContactGroup)
231
+
232
+
233
+ ### For queries that match over 200 records
234
+
235
+ For queries that span multiple requests (and you want all records at once)
236
+ use {Eloqua::Query#each_page} each page functions just like each but
237
+ will make consecutive requests to fetch all pages. It also takes an
238
+ optional max pages parameter which allows you to limit the number of
239
+ pages to fetch and/or pause and resume requests.
240
+
241
+ ## TODO
242
+
243
+ - (inline) Docs [DONE FOR QUERY]
244
+ - Guide
245
+ - Email API (ongoing)
@@ -0,0 +1,13 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+ Bundler.setup(:default, :test)
8
+
9
+
10
+ Dir["lib/tasks**/*.rake"].each do |file|
11
+ puts file
12
+ load(file)
13
+ end
data/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ - Improved SSL for specifying the map of the element and what
2
+ importer/export to use.
3
+
4
+ - Rename 'map' to 'field'
5
+
6
+ - Shared Spec helper for better BDD. Currently I run into issues from
7
+ duplicating from 'map' and the fact there is no easy way to test the
8
+ mappings themselves.
@@ -0,0 +1,32 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "eloqua/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'eloqua'
6
+ s.version = Eloqua::VERSION
7
+ s.date = '2012-07-04'
8
+ s.authors = ["Brian Jou", "Ryan Caught", "James Lal"]
9
+ s.email = 'brian@vidyard.com'
10
+ s.files = ["lib/eloqua.rb"]
11
+ s.summary = 'Eloqua API for Ruby'
12
+ s.description = 'An Eloqua API Wrapper for Ruby'
13
+
14
+ s.add_runtime_dependency 'savon'
15
+ s.add_runtime_dependency 'builder'
16
+ s.add_runtime_dependency 'activemodel'
17
+ s.add_runtime_dependency 'activesupport', '= 3.0.6'
18
+ s.add_runtime_dependency 'i18n', '= 0.5.0'
19
+ s.add_development_dependency 'watchr'
20
+ s.add_development_dependency 'ruby-fsevent'
21
+ s.add_development_dependency 'ruby-debug19'
22
+ s.add_development_dependency 'savon_spec'
23
+ s.add_development_dependency 'rspec'
24
+ s.add_development_dependency 'flexmock'
25
+ s.add_development_dependency 'timecop'
26
+ s.add_development_dependency 'ci_reporter'
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
31
+ s.require_paths = ["lib"]
32
+ end