activerecord-activesalesforce-adapter 2.0.0

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