eloqua 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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