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,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
+