activerecord-activesalesforce-adapter 2.0.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 (33) hide show
  1. data/README +51 -0
  2. data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
  3. data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +777 -0
  4. data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
  5. data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
  6. data/lib/active_record/connection_adapters/column_definition.rb +95 -0
  7. data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
  8. data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
  9. data/lib/active_record/connection_adapters/recording_binding.rb +90 -0
  10. data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
  11. data/lib/active_record/connection_adapters/result_array.rb +31 -0
  12. data/lib/active_record/connection_adapters/rforce.rb +361 -0
  13. data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
  14. data/test/unit/basic_test.rb +203 -0
  15. data/test/unit/config.yml +5 -0
  16. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
  17. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
  18. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
  19. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
  20. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
  21. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
  22. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
  23. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
  24. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
  25. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
  26. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
  27. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
  28. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
  29. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
  30. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
  31. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
  32. data/test/unit/recorded_test_case.rb +83 -0
  33. metadata +105 -0
@@ -0,0 +1,31 @@
1
+ =begin
2
+ ActiveSalesforce
3
+ Copyright 2006 Doug Chasman
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+ require 'pp'
19
+
20
+
21
+ module ActiveSalesforce
22
+
23
+ class ResultArray < Array
24
+ attr_reader :actual_size
25
+
26
+ def initialize(actual_size)
27
+ @actual_size = actual_size
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,361 @@
1
+ =begin
2
+ RForce v0.1
3
+ Copyright (c) 2005 Ian Dees
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+ =end
23
+
24
+ # RForce is a simple Ruby binding to the SalesForce CRM system.
25
+ # Rather than enforcing adherence to the sforce.com schema,
26
+ # RForce assumes you are familiar with the API. Ruby method names
27
+ # become SOAP method names. Nested Ruby hashes become nested
28
+ # XML elements.
29
+ #
30
+ # Example:
31
+ #
32
+ # binding = RForce::Binding.new 'na1-api.salesforce.com'
33
+ # binding.login 'username', 'password'
34
+ # answer = binding.search(
35
+ # :searchString =>
36
+ # 'find {Some Account Name} in name fields returning account(id)')
37
+ # account_id = answer.searchResponse.result.searchRecords.record.Id
38
+ #
39
+ # opportunity = {
40
+ # :accountId => account_id,
41
+ # :amount => "10.00",
42
+ # :name => "New sale",
43
+ # :closeDate => "2005-09-01",
44
+ # :stageName => "Closed Won"
45
+ # }
46
+ #
47
+ # binding.create 'sObject {"xsi:type" => "Opportunity"}' => opportunity
48
+ #
49
+
50
+
51
+ require 'net/https'
52
+ require 'uri'
53
+ require 'zlib'
54
+ require 'stringio'
55
+ require 'rexml/document'
56
+ require 'rexml/xpath'
57
+ require 'rubygems'
58
+ gem 'builder', ">= 2.0.0"
59
+
60
+
61
+ module RForce
62
+
63
+ #Allows indexing hashes like method calls: hash.key
64
+ #to supplement the traditional way of indexing: hash[key]
65
+ module FlashHash
66
+ def method_missing(method, *args)
67
+ self[method]
68
+ end
69
+ end
70
+
71
+ #Turns an XML response from the server into a Ruby
72
+ #object whose methods correspond to nested XML elements.
73
+ class SoapResponse
74
+ include FlashHash
75
+
76
+ #Parses an XML string into structured data.
77
+ def initialize(content)
78
+ document = REXML::Document.new content
79
+ node = REXML::XPath.first document, '//soapenv:Body'
80
+ @parsed = SoapResponse.parse node
81
+ end
82
+
83
+ #Allows this object to act like a hash (and therefore
84
+ #as a FlashHash via the include above).
85
+ def [](symbol)
86
+ @parsed[symbol]
87
+ end
88
+
89
+ #Digests an XML DOM node into nested Ruby types.
90
+ def SoapResponse.parse(node)
91
+ #Convert text nodes into simple strings.
92
+ return node.text unless node.has_elements?
93
+
94
+ #Convert nodes with children into FlashHashes.
95
+ elements = {}
96
+ class << elements
97
+ include FlashHash
98
+ end
99
+
100
+ #Add all the element's children to the hash.
101
+ node.each_element do |e|
102
+ name = e.name.to_sym
103
+
104
+ case elements[name]
105
+ #The most common case: unique child element tags.
106
+ when NilClass: elements[name] = parse(e)
107
+
108
+ #Non-unique child elements become arrays:
109
+
110
+ #We've already created the array: just
111
+ #add the element.
112
+ when Array: elements[name] << parse(e)
113
+
114
+ #We haven't created the array yet: do so,
115
+ #then put the existing element in, followed
116
+ #by the new one.
117
+ else
118
+ elements[name] = [elements[name]]
119
+ elements[name] << parse(e)
120
+ end
121
+ end
122
+
123
+ return elements
124
+ end
125
+ end
126
+
127
+
128
+ #Implements the connection to the SalesForce server.
129
+ class Binding
130
+ DEFAULT_BATCH_SIZE = 10
131
+ attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
132
+ :trigger_other_email, :trigger_auto_response_email
133
+
134
+ #Fill in the guts of this typical SOAP envelope
135
+ #with the session ID and the body of the SOAP request.
136
+ Envelope = <<-HERE
137
+ <?xml version="1.0" encoding="utf-8" ?>
138
+ <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
139
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
140
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
141
+ xmlns:partner="urn:partner.soap.sforce.com">
142
+ xmlns:spartner="urn:sobject.partner.soap.sforce.com">
143
+ <soap:Header>
144
+ <partner:SessionHeader soap:mustUnderstand='1'>
145
+ <partner:sessionId>%s</partner:sessionId>
146
+ </partner:SessionHeader>
147
+ <partner:QueryOptions soap:mustUnderstand='1'>
148
+ <partner:batchSize>%d</partner:batchSize>
149
+ </partner:QueryOptions>
150
+ %s
151
+ </soap:Header>
152
+ <soap:Body>
153
+ %s
154
+ </soap:Body>
155
+ </soap:Envelope>
156
+ HERE
157
+
158
+ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
159
+ AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
160
+ MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
161
+ ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
162
+
163
+ #Connect to the server securely.
164
+ def initialize(url, sid)
165
+ init_server(url)
166
+
167
+ @session_id = sid
168
+ @batch_size = DEFAULT_BATCH_SIZE
169
+ end
170
+
171
+
172
+ def show_debug
173
+ ENV['SHOWSOAP'] == 'true'
174
+ end
175
+
176
+
177
+ def init_server(url)
178
+ @url = URI.parse(url)
179
+ @server = Net::HTTP.new(@url.host, @url.port)
180
+ @server.use_ssl = @url.scheme == 'https'
181
+ @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
182
+
183
+ # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
184
+ @server.set_debug_output $stderr if show_debug
185
+ end
186
+
187
+
188
+ #Log in to the server and remember the session ID
189
+ #returned to us by SalesForce.
190
+ def login(user, password)
191
+ @user = user
192
+ @password = password
193
+
194
+ response = call_remote(:login, [:username, user, :password, password])
195
+
196
+ raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
197
+
198
+ result = response[:loginResponse][:result]
199
+ @session_id = result[:sessionId]
200
+
201
+ init_server(result[:serverUrl])
202
+
203
+ response
204
+ end
205
+
206
+
207
+ #Call a method on the remote server. Arguments can be
208
+ #a hash or (if order is important) an array of alternating
209
+ #keys and values.
210
+ def call_remote(method, args)
211
+ #Create XML text from the arguments.
212
+ expanded = ''
213
+ @builder = Builder::XmlMarkup.new(:target => expanded)
214
+ expand({method => args}, 'urn:partner.soap.sforce.com')
215
+
216
+ extra_headers = ""
217
+ extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
218
+ extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
219
+ extra_headers << MruHeader if update_mru
220
+ extra_headers << (ClientIdHeader % client_id) if client_id
221
+
222
+ if trigger_user_email or trigger_other_email or trigger_auto_response_email
223
+ extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
224
+
225
+ extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
226
+ extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
227
+ extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
228
+
229
+ extra_headers << '</partner:EmailHeader>'
230
+ end
231
+
232
+ #Fill in the blanks of the SOAP envelope with our
233
+ #session ID and the expanded XML of our request.
234
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
235
+
236
+ # reset the batch size for the next request
237
+ @batch_size = DEFAULT_BATCH_SIZE
238
+
239
+ # gzip request
240
+ request = encode(request)
241
+
242
+ headers = {
243
+ 'Connection' => 'Keep-Alive',
244
+ 'Content-Type' => 'text/xml',
245
+ 'SOAPAction' => '""',
246
+ 'User-Agent' => 'activesalesforce rforce/1.0'
247
+ }
248
+
249
+ unless show_debug
250
+ headers['Accept-Encoding'] = 'gzip'
251
+ headers['Content-Encoding'] = 'gzip'
252
+ end
253
+
254
+ #Send the request to the server and read the response.
255
+ response = @server.post2(@url.path, request.lstrip, headers)
256
+
257
+ # decode if we have encoding
258
+ content = decode(response)
259
+
260
+ # Check to see if INVALID_SESSION_ID was raised and try to relogin in
261
+ if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
262
+ login(@user, @password)
263
+
264
+ # repackage and rencode request with the new session id
265
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
266
+ request = encode(request)
267
+
268
+ #Send the request to the server and read the response.
269
+ response = @server.post2(@url.path, request.lstrip, headers)
270
+
271
+ content = decode(response)
272
+ end
273
+
274
+ SoapResponse.new(content)
275
+ end
276
+
277
+
278
+ # decode gzip
279
+ def decode(response)
280
+ encoding = response['Content-Encoding']
281
+
282
+ # return body if no encoding
283
+ if !encoding then return response.body end
284
+
285
+ # decode gzip
286
+ case encoding.strip
287
+ when 'gzip':
288
+ begin
289
+ gzr = Zlib::GzipReader.new(StringIO.new(response.body))
290
+ decoded = gzr.read
291
+ ensure
292
+ gzr.close
293
+ end
294
+ decoded
295
+ else
296
+ response.body
297
+ end
298
+ end
299
+
300
+
301
+ # encode gzip
302
+ def encode(request)
303
+ return request if show_debug
304
+
305
+ begin
306
+ ostream = StringIO.new
307
+ gzw = Zlib::GzipWriter.new(ostream)
308
+ gzw.write(request)
309
+ ostream.string
310
+ ensure
311
+ gzw.close
312
+ end
313
+ end
314
+
315
+
316
+ #Turns method calls on this object into remote SOAP calls.
317
+ def method_missing(method, *args)
318
+ unless args.size == 1 && [Hash, Array].include?(args[0].class)
319
+ raise 'Expected 1 Hash or Array argument'
320
+ end
321
+
322
+ call_remote method, args[0]
323
+ end
324
+
325
+
326
+ #Expand Ruby data structures into XML.
327
+ def expand(args, xmlns = nil)
328
+ #Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
329
+ if (args.class == Array)
330
+ args.each_index{|i| args[i, 2] = [args[i, 2]]}
331
+ end
332
+
333
+ args.each do |key, value|
334
+ attributes = xmlns ? {:xmlns => xmlns} : {}
335
+
336
+ #If the XML tag requires attributes,
337
+ #the tag name will contain a space
338
+ #followed by a string representation
339
+ #of a hash of attributes.
340
+ #
341
+ #e.g. 'sObject {"xsi:type" => "Opportunity"}'
342
+ #becomes <sObject xsi:type="Opportunity>...</sObject>
343
+ if key.is_a? String
344
+ key, modifier = key.split(' ', 2)
345
+
346
+ attributes.merge!(eval(modifier)) if modifier
347
+ end
348
+
349
+ #Create an XML element and fill it with this
350
+ #value's sub-items.
351
+ case value
352
+ when Hash, Array
353
+ @builder.tag!(key, attributes) do expand value; end
354
+
355
+ when String
356
+ @builder.tag!(key, attributes) { @builder.text! value }
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,57 @@
1
+ =begin
2
+ ActiveSalesforce
3
+ Copyright 2006 Doug Chasman
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+ require 'set'
19
+ require 'pp'
20
+
21
+
22
+ module ActiveSalesforce
23
+
24
+ class SessionIDAuthenticationFilter
25
+ @@klasses = Set.new
26
+
27
+
28
+ def self.register(klass)
29
+ @@klasses.add(klass)
30
+ end
31
+
32
+
33
+ def self.filter(controller)
34
+ # Look to see if a SID was passed in the URL
35
+ params = controller.params
36
+ sid = params[:sid]
37
+
38
+ if sid
39
+ api_server_url = params[:api_server_url]
40
+
41
+ # Iterate over all classes that have registered for SID auth support
42
+ connection = nil
43
+ @@klasses.each do |klass|
44
+ unless connection
45
+ klass.establish_connection(:adapter => 'activesalesforce', :sid => sid, :url => api_server_url)
46
+ connection = klass.connection
47
+ else
48
+ klass = connection
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+