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,31 @@
1
+ require 'eloqua/remote_object'
2
+
3
+ module Eloqua
4
+
5
+ class Asset < RemoteObject
6
+
7
+ self.remote_group = :asset
8
+
9
+ def add_member(entity)
10
+ member_operation(:add_group_member, entity)
11
+ end
12
+
13
+ def remove_member(entity)
14
+ member_operation(:remove_group_member, entity)
15
+ end
16
+
17
+ private
18
+
19
+ def member_operation(method, entity)
20
+ unless (entity.is_a?(Eloqua::Entity))
21
+ raise(ArgumentError, "Must pass a Eloqua::Entity")
22
+ end
23
+ unless (entity.persisted?)
24
+ raise(ArgumentError, "Cannot add member Entity has not been saved. (!entity.persisted?")
25
+ end
26
+ api.send(method, remote_type, id, entity.remote_type, entity.id)
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,31 @@
1
+ module Eloqua
2
+ module Builder
3
+
4
+ module Templates
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :builder_templates
10
+ self.builder_templates = {}
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def builder_template(name, *args)
16
+ template = builder_templates[name]
17
+ Proc.new do |xml|
18
+ template.call(xml, *args)
19
+ end
20
+ end
21
+
22
+ def define_builder_template(name, &block)
23
+ builder_templates[name] = block
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+
30
+ end
31
+ end
@@ -0,0 +1,129 @@
1
+ require 'eloqua/builder/templates'
2
+
3
+ module Eloqua
4
+
5
+ module Builder
6
+
7
+ # This could (and likely should) be submitted as a patch for
8
+ # the main builder class
9
+ class Xml < ::Builder::XmlMarkup
10
+
11
+ include Eloqua::Builder::Templates
12
+
13
+ # XML Templates
14
+
15
+ # For use with strings and integers may do strange
16
+ # things on the SOAP server side if given a float
17
+ define_builder_template :array do |xml, array|
18
+ array.each do |element|
19
+ tag = 'string'
20
+ if(element.is_a?(String))
21
+ tag = 'string'
22
+ elsif(element.is_a?(Numeric))
23
+ tag = 'int'
24
+ end
25
+ xml.arr(tag.to_sym, element)
26
+ end
27
+ end
28
+
29
+ define_builder_template :int_array do |xml, array|
30
+ array.each do |element|
31
+ unless(element.is_a?(Numeric))
32
+ element = element.to_i
33
+ if(element == 0 || !element)
34
+ next
35
+ end
36
+ end
37
+ xml.arr(:int, element)
38
+ end
39
+ end
40
+
41
+ # For use with add/remove membership
42
+ define_builder_template :object do |xml, object_type, type, id|
43
+ xml.tag!(object_type) do
44
+ xml.object_type!(object_type) do
45
+ xml.template!(:object_type, type)
46
+ end
47
+ xml.Id(id)
48
+ end
49
+ end
50
+
51
+ # For use with the entity function
52
+ define_builder_template :object_type do |xml, object|
53
+ xml.ID(object[:id])
54
+ xml.Name(object[:name])
55
+ xml.Type(object[:type])
56
+ end
57
+
58
+ # defines entity attribute fields for use in update/create
59
+ define_builder_template :fields do |xml, object_type, entity_attributes|
60
+ entity_attributes.each do |attribute, value|
61
+ xml.tag!("#{object_type.to_s.camelize}Fields") do
62
+ xml.InternalName(attribute.to_s)
63
+ xml.Value(value)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Dynamic entity for update/create/etc...
69
+
70
+ define_builder_template :dynamic do |xml, object_type, type, id, attributes|
71
+ xml.tag!("#{object_type.to_s.camelize}Type") do
72
+ xml.template!(:object_type, type)
73
+ end
74
+
75
+ xml.FieldValueCollection do
76
+ xml.template!(:fields, object_type, attributes)
77
+ end
78
+
79
+ xml.Id(id) if id
80
+ end
81
+
82
+ delegate :builder_template, :to => self
83
+
84
+
85
+ def initialize(options = {}, &block)
86
+ super
87
+ @namespace = nil
88
+ @namespace = options[:namespace].to_sym if options[:namespace]
89
+ yield self if block_given?
90
+ end
91
+
92
+ def self.create(options = {}, &block)
93
+ new(options, &block).target!
94
+ end
95
+
96
+ def template!(template, *args)
97
+ builder_template(template, *args).call(self)
98
+ end
99
+
100
+ def dynamic_object!(sym, *args, &block)
101
+ tag!("Dynamic#{sym.to_s.camelize}", *args, &block)
102
+ end
103
+
104
+ def object_type!(sym, *args, &block)
105
+ tag!("#{sym.to_s.camelize}Type", *args, &block)
106
+ end
107
+
108
+ def object_type_lower!(sym, *args, &block)
109
+ tag!("#{sym}Type", *args, &block)
110
+ end
111
+
112
+ def object_collection!(sym, *args, &block)
113
+ tag!("#{sym.to_s.pluralize.downcase}", *args, &block)
114
+ end
115
+
116
+ # Extend to allow default namespace
117
+ def method_missing(sym, *args, &block)
118
+ if(@namespace && !args.first.kind_of?(Symbol))
119
+ args.unshift(sym.to_sym)
120
+ sym = @namespace
121
+ end
122
+ super(sym, *args, &block)
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,72 @@
1
+ require 'eloqua/remote_object'
2
+
3
+ module Eloqua
4
+
5
+ class Entity < RemoteObject
6
+
7
+ self.remote_group = :entity
8
+
9
+ # Returns an :id indexed list of memberships for contact
10
+ #
11
+ # # Example output
12
+ # {'1' => {:id => '1', :name => 'Contact Group Name', :type => 'ContactGroup}}
13
+ #
14
+ # @return [Hash] Integer => Hash
15
+ def list_memberships
16
+ self.class.list_memberships(id)
17
+ end
18
+
19
+ def add_membership(asset)
20
+ asset.add_member(self)
21
+ end
22
+
23
+ def remove_membership(asset)
24
+ asset.remove_member(self)
25
+ end
26
+
27
+ class << self
28
+
29
+ # Returns an :id indexed list of memberships for given contact id
30
+ #
31
+ # # Example output
32
+ # {'1' => {:id => '1', :name => 'Contact Group Name', :type => 'ContactGroup}}
33
+ #
34
+ # @param [String, Integer] contact id
35
+ # @return [Hash] Integer => Hash
36
+ def list_memberships(id)
37
+ memberships = api.list_memberships(remote_type, id)
38
+
39
+ if(memberships && !memberships.empty?)
40
+ memberships.inject({}) do |map, membership|
41
+ map[membership[:id]] = membership
42
+ map
43
+ end
44
+ else
45
+ memberships || {}
46
+ end
47
+
48
+ end
49
+
50
+ def where(conditions = nil, fields = [], limit = 200, page = 1)
51
+ if(conditions)
52
+ query = where
53
+ conditions.each do |key, value|
54
+ query.on(key, '=', value)
55
+ end
56
+ query.fields(fields) if fields
57
+ query.limit(limit)
58
+ query.page(page)
59
+ results = query.all
60
+ if(results.blank?)
61
+ false
62
+ else
63
+ results
64
+ end
65
+ else
66
+ Eloqua::Query.new(self)
67
+ end
68
+ end
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ class Eloqua::RemoteError < StandardError; end
2
+ class Eloqua::DuplicateRecordError < Eloqua::RemoteError; end
3
+
4
+ class Eloqua::SoapError < Savon::SOAP::Fault; end
5
+ class Eloqua::HTTPError < Savon::HTTP::Error; end
@@ -0,0 +1,78 @@
1
+ module Eloqua
2
+ module Helper
3
+
4
+ module AttributeMap
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :attribute_map, :attribute_map_reverse
10
+
11
+ attr_reader :instance_reverse_keys
12
+
13
+ self.attribute_map = {}.with_indifferent_access
14
+ self.attribute_map_reverse = {}.with_indifferent_access
15
+
16
+ class << self
17
+ alias_method_chain :inherited, :clone_attributes
18
+ end
19
+
20
+ end
21
+
22
+ module ClassMethods
23
+
24
+ def inherited_with_clone_attributes(klass)
25
+ klass.attribute_map = attribute_map.clone
26
+ klass.attribute_map_reverse = attribute_map_reverse.clone
27
+ inherited_without_clone_attributes(klass) if method_defined?(:inherited_without_clone_attributes)
28
+ end
29
+
30
+ def eloqua_attribute(attribute)
31
+ (attribute_map_reverse.fetch(attribute) { attribute }).to_s
32
+ end
33
+
34
+ def map_attribute(attribute)
35
+ attribute_map.fetch(attribute) { attribute.to_s }
36
+ end
37
+
38
+ # This shoud always be used over directly editing attribute_map
39
+ def map(hash)
40
+ hash.each do |key, value|
41
+ value = value.to_sym
42
+ key = key.to_sym
43
+
44
+ attribute_map[key] = value
45
+ attribute_map_reverse[value] = key
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ module InstanceMethods
52
+
53
+ def map_attributes(attributes)
54
+ @instance_reverse_keys ||= attribute_map_reverse.clone
55
+ results = {}.with_indifferent_access
56
+
57
+ attributes.each do |key, value|
58
+ formatted_key = attribute_map.fetch(key) { key.to_s.gsub(/^C_/, '').underscore }
59
+ @instance_reverse_keys[formatted_key] = key
60
+ results[formatted_key] = value
61
+ end
62
+ results
63
+ end
64
+
65
+ def reverse_map_attributes(attributes)
66
+ results = {}.with_indifferent_access
67
+ attributes.each do |key, value|
68
+ results[@instance_reverse_keys.fetch(key){key}] = value
69
+ end
70
+ results
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,291 @@
1
+ require 'eloqua/api/service'
2
+ require 'eloqua/remote_object'
3
+
4
+ module Eloqua
5
+ class Query
6
+
7
+ delegate :api, :to => :remote_object
8
+
9
+ attr_reader :collection, :remote_object, :conditions, :total_pages
10
+ attr_internal :current_page, :query_started
11
+
12
+
13
+ # The amount of time in seconds to wait before sending another request to Eloqua
14
+ @@request_delay = 1
15
+ cattr_accessor :request_delay
16
+
17
+ # Create a new query to attach conditions to.
18
+ #
19
+ # class Contact < Eloqua::Entity
20
+ # remote_type = api.remote_type('Contact')
21
+ # end
22
+ #
23
+ # Eloqua::Query.new(Contact)
24
+ #
25
+ # @param [Eloqua::RemoteObject] or one of its descendants
26
+ def initialize(remote_object)
27
+ unless(remote_object.is_a?(Class) && Eloqua::RemoteObject >= remote_object)
28
+ raise(ArgumentError, 'must provide an Eloqua::RemoteObject or one of its descendants')
29
+ end
30
+
31
+ @page = 1
32
+ @limit = 200
33
+ @collection = []
34
+ @remote_object = remote_object
35
+ @conditions = []
36
+ @fields = nil
37
+ @has_requested = false
38
+ end
39
+
40
+
41
+ ## CHAIN-ABLES they reset the has_requested? but do not clear the collection
42
+
43
+ # Sets or gets limit
44
+ #
45
+ # query.limit(5) # sets limit returns self
46
+ # query.limit # returns 5
47
+ #
48
+ # @param [Integer]
49
+ # @return [self, Integer]
50
+ def limit(value = nil); end
51
+
52
+ # Sets or gets page
53
+ #
54
+ # query.page(5) # sets limit returns self
55
+ # query.page # returns 5
56
+ #
57
+ # @param [Integer]
58
+ # @return [self, Integer]
59
+ #
60
+ def page(value = nil); end
61
+
62
+ # Sets or gets the array of fields to find.
63
+ # Can use a literal string or a symbol of a {Eloqua::RemoteObject#map mapped} attribute
64
+ #
65
+ # query.fields([:email, 'Date'])
66
+ # query.fields # returns [:email, 'Date']
67
+ #
68
+ # @see Eloqua::RemoteObject::map
69
+ # @param [Array]
70
+ # @return [self, Array]
71
+ def fields(value = nil); end
72
+
73
+
74
+ # This mess defines the limit and page getter/setter methods
75
+ # when they are set they will also set #has_requested? to false
76
+ [:fields, :limit, :page].each do |attr|
77
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
78
+ remove_method(:#{attr})
79
+ def #{attr}(value = nil)
80
+ if(value.nil?)
81
+ @#{attr}
82
+ else
83
+ @has_requested = false
84
+ @#{attr} = value
85
+ self
86
+ end
87
+ end
88
+ RUBY
89
+ end
90
+
91
+ # Clears all conditions added by {on}
92
+ #
93
+ # query.on(:id, '>', '1')
94
+ # query.clear_conditions!
95
+ #
96
+ def clear_conditions!
97
+ @has_requested = false
98
+ conditions.clear
99
+ end
100
+
101
+ # Adds a condition to the query; may be chained.
102
+ #
103
+ # query.on(:email, '=', 'value').on('created_at', '>', '2011-04-20')
104
+ #
105
+ # @param [String, Symbol] field name
106
+ # @param [String] operator can use: `[ =, !=, <, >, >=, <=]`
107
+ # @param [String] value to search for
108
+ # @return self
109
+ def on(field, operator, value)
110
+ @has_requested = false
111
+ @conditions << {
112
+ :field => field,
113
+ :type => operator,
114
+ :value => value
115
+ }
116
+ self
117
+ end
118
+
119
+
120
+ # Send the built request to eloqua
121
+ #
122
+ # query.on(:email, '=', '*') # wildcard
123
+ # query.request!
124
+ # query.collection # Array of results
125
+ # query.current_page # Current page
126
+ # query.total_pages # Number of pages
127
+ #
128
+ # @return self
129
+ def request!
130
+ return if has_requested?
131
+ sleep_for = wait_for_request_delay
132
+ if(sleep_for)
133
+ sleep(sleep_for)
134
+ end
135
+ xml_query = api.builder do |xml|
136
+ xml.eloquaType do
137
+ xml.template!(:object_type, remote_object.remote_type)
138
+ end
139
+ xml.searchQuery(build_query)
140
+
141
+ # This won't harm anything but it is changing the
142
+ # fields to its mapped elouqa name.
143
+ if(!fields.blank? && fields.is_a?(Array))
144
+ fields.map! do |field|
145
+ field = remote_object.eloqua_attribute(field)
146
+ end
147
+ xml.fieldNames do
148
+ xml.template!(:array, fields)
149
+ end
150
+ end
151
+
152
+ xml.pageNumber(page)
153
+ xml.pageSize(limit)
154
+ end
155
+
156
+ # Add timestamp of when we made request
157
+ @_query_started = Time.now
158
+ result = api.request(:query, xml_query)
159
+
160
+ # Clear collection
161
+ collection.clear
162
+
163
+ # Mark as requested
164
+ @has_requested = true
165
+
166
+
167
+ if(result[:entities])
168
+ @total_pages = result[:total_pages].to_i
169
+ entities = Eloqua.format_results_for_array(result, :entities, :dynamic_entity)
170
+ records = entities.inject([]) do |records, entity|
171
+ record_attrs = {}
172
+ entity_id = entity[:id]
173
+ entity[:field_value_collection][:entity_fields].each do |entity_attr|
174
+ record_attrs[entity_attr[:internal_name]] = entity_attr[:value]
175
+ end
176
+ record_attrs[remote_object.primary_key] = entity_id
177
+ record_object = remote_object.new(record_attrs, :remote)
178
+
179
+ collection << record_object
180
+ end
181
+ collection
182
+ else
183
+ @total_pages = 0
184
+ false
185
+ end
186
+ end
187
+
188
+ def wait_for_request_delay
189
+ if(request_delay && query_started)
190
+ now = Time.now
191
+ wait_until = Time.at((request_delay + query_started.to_f))
192
+ if(wait_until > now)
193
+ wait_until - now
194
+ end
195
+ else
196
+ false
197
+ end
198
+ end
199
+
200
+
201
+ # Has the request been made yet?
202
+ #
203
+ # query.has_requested? # false
204
+ # query.on(:email, '=', '*').request!
205
+ # query.has_requested? # true
206
+ #
207
+ # @return [Boolean]
208
+ def has_requested?
209
+ @has_requested
210
+ end
211
+
212
+ # Sends request if not already set and iterates through result
213
+ #
214
+ # query.each do |record|
215
+ # record.class # query.remote_object
216
+ # end
217
+ #
218
+ # Currently this is a shortcut for
219
+ #
220
+ # query.all.each do |record|
221
+ # ...
222
+ # end
223
+ #
224
+ # @param [Proc] a block iterator
225
+ def each(&block)
226
+ all.each(&block)
227
+ end
228
+
229
+ # Sends request and returns collection
230
+ #
231
+ # query.on(:email, '=', '*').all # => [Eloqua::RemoteObject.new(), ...]
232
+ #
233
+ # @return [Array] collection of Eloqua::RemoteObject
234
+ def all
235
+ request!
236
+ collection
237
+ end
238
+
239
+ # Iterates through each page up to max_pages
240
+ # when max_pages is nil (default) will iterate through
241
+ # each page yielding a block with a record.
242
+ #
243
+ # with max_pages you could and then resume the loop through
244
+ # pages
245
+ #
246
+ # query.each_page(2) |record|
247
+ # query.total_pages # 10
248
+ # query.page # 1 ... 2
249
+ # end
250
+ #
251
+ # ...
252
+ #
253
+ # query.each_page(2) |record|
254
+ # query.total_pages # 10
255
+ # query.page # 3 ... 4
256
+ # end
257
+ #
258
+ # @see Query#each
259
+ # @param [Integer] max pages to iterate through
260
+ def each_page(max_pages = nil, &block)
261
+ each(&block)
262
+ while(total_pages > page)
263
+ break if max_pages && page >= max_pages
264
+ page(page + 1)
265
+ each(&block)
266
+ end
267
+ end
268
+
269
+ protected
270
+
271
+ # Builds query from conditions
272
+ # conditions are assembled by their field, type and then value as below
273
+ #
274
+ # #{field}#{type}'#{value}'
275
+ #
276
+ # then joined by AND which acts more like an SQL "OR" in Eloqua
277
+ # > and < are escaped with &gt; and &lt;
278
+ def build_query
279
+ conditions.inject([]) do |parts, cond|
280
+ part = ""
281
+ part << remote_object.eloqua_attribute(cond[:field])
282
+ part << cond[:type].to_s
283
+ part << "'#{cond[:value]}'"
284
+ parts << part
285
+ end.join(" AND ")
286
+ end
287
+
288
+
289
+ end
290
+ end
291
+