althor880-activerecord-activesalesforce-adapter 2.3.5

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 (45) hide show
  1. data/.gitignore +1 -0
  2. data/README +66 -0
  3. data/Rakefile +20 -0
  4. data/VERSION +1 -0
  5. data/althor880-activerecord-activesalesforce-adapter.gemspec +113 -0
  6. data/lib/active_record/connection_adapters/activesalesforce.rb +36 -0
  7. data/lib/active_record/connection_adapters/activesalesforce_adapter.rb +824 -0
  8. data/lib/active_record/connection_adapters/asf_active_record.rb +40 -0
  9. data/lib/active_record/connection_adapters/boxcar_command.rb +66 -0
  10. data/lib/active_record/connection_adapters/column_definition.rb +95 -0
  11. data/lib/active_record/connection_adapters/entity_definition.rb +59 -0
  12. data/lib/active_record/connection_adapters/id_resolver.rb +84 -0
  13. data/lib/active_record/connection_adapters/recording_binding.rb +90 -0
  14. data/lib/active_record/connection_adapters/relationship_definition.rb +81 -0
  15. data/lib/active_record/connection_adapters/result_array.rb +31 -0
  16. data/lib/active_record/connection_adapters/sid_authentication_filter.rb +57 -0
  17. data/lib/activerecord-activesalesforce-adapter.rb +1 -0
  18. data/lib/rforce.rb +116 -0
  19. data/lib/rforce/binding.rb +202 -0
  20. data/lib/rforce/method_keys.rb +14 -0
  21. data/lib/rforce/soap_pullable.rb +93 -0
  22. data/lib/rforce/soap_response_expat.rb +35 -0
  23. data/lib/rforce/soap_response_hpricot.rb +70 -0
  24. data/lib/rforce/soap_response_rexml.rb +34 -0
  25. data/lib/rforce/version.rb +3 -0
  26. data/test/unit/basic_test.rb +204 -0
  27. data/test/unit/config.yml +5 -0
  28. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
  29. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
  30. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
  31. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
  32. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
  33. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
  34. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
  35. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
  36. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
  37. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
  38. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
  39. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
  40. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
  41. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
  42. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
  43. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
  44. data/test/unit/recorded_test_case.rb +83 -0
  45. metadata +160 -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,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
+
@@ -0,0 +1 @@
1
+ require 'active_record/connection_adapters/activesalesforce_adapter.rb'
data/lib/rforce.rb ADDED
@@ -0,0 +1,116 @@
1
+ =begin
2
+ RForce v0.3
3
+ Copyright (c) 2005-2008 Ian Dees and contributors
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
+
56
+ require 'rubygems'
57
+
58
+ gem 'builder', '>= 2.0.0'
59
+ require 'builder'
60
+
61
+ gem 'facets', '>= 2.4'
62
+ require 'facets/openhash'
63
+
64
+ require 'rforce/binding'
65
+ require 'rforce/soap_response_rexml'
66
+ require 'rforce/soap_response_hpricot' rescue nil
67
+ require 'rforce/soap_response_expat' rescue nil
68
+
69
+
70
+ module RForce
71
+ # Use the fastest XML parser available.
72
+ def self.parser(name)
73
+ RForce.const_get(name) rescue nil
74
+ end
75
+
76
+ SoapResponse =
77
+ parser(:SoapResponseExpat) ||
78
+ parser(:SoapResponseHpricot) ||
79
+ SoapResponseRexml
80
+
81
+ # Expand Ruby data structures into XML.
82
+ def expand(builder, args, xmlns = nil)
83
+ # Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
84
+ if (args.class == Array)
85
+ args.each_index{|i| args[i, 2] = [args[i, 2]]}
86
+ end
87
+
88
+ args.each do |key, value|
89
+ attributes = xmlns ? {:xmlns => xmlns} : {}
90
+
91
+ # If the XML tag requires attributes,
92
+ # the tag name will contain a space
93
+ # followed by a string representation
94
+ # of a hash of attributes.
95
+ #
96
+ # e.g. 'sObject {"xsi:type" => "Opportunity"}'
97
+ # becomes <sObject xsi:type="Opportunity>...</sObject>
98
+ if key.is_a? String
99
+ key, modifier = key.split(' ', 2)
100
+
101
+ attributes.merge!(eval(modifier)) if modifier
102
+ end
103
+
104
+ # Create an XML element and fill it with this
105
+ # value's sub-items.
106
+ case value
107
+ when Hash, Array
108
+ builder.tag!(key, attributes) do expand builder, value; end
109
+
110
+ when String
111
+ builder.tag!(key, attributes) { builder.text! value }
112
+ end
113
+ end
114
+ end
115
+
116
+ end
@@ -0,0 +1,202 @@
1
+ module RForce
2
+ # Implements the connection to the SalesForce server.
3
+ class Binding
4
+ include RForce
5
+
6
+ DEFAULT_BATCH_SIZE = 10
7
+ attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
8
+ :trigger_other_email, :trigger_auto_response_email
9
+
10
+ # Fill in the guts of this typical SOAP envelope
11
+ # with the session ID and the body of the SOAP request.
12
+ Envelope = <<-HERE
13
+ <?xml version="1.0" encoding="utf-8" ?>
14
+ <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
15
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
16
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
17
+ xmlns:partner="urn:partner.soap.sforce.com">
18
+ xmlns:spartner="urn:sobject.partner.soap.sforce.com">
19
+ <soap:Header>
20
+ <partner:SessionHeader soap:mustUnderstand='1'>
21
+ <partner:sessionId>%s</partner:sessionId>
22
+ </partner:SessionHeader>
23
+ <partner:QueryOptions soap:mustUnderstand='1'>
24
+ <partner:batchSize>%d</partner:batchSize>
25
+ </partner:QueryOptions>
26
+ %s
27
+ </soap:Header>
28
+ <soap:Body>
29
+ %s
30
+ </soap:Body>
31
+ </soap:Envelope>
32
+ HERE
33
+
34
+ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
35
+ AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
36
+ MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
37
+ ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
38
+
39
+ # Connect to the server securely.
40
+ def initialize(url, sid = nil)
41
+ init_server(url)
42
+
43
+ @session_id = sid
44
+ @batch_size = DEFAULT_BATCH_SIZE
45
+ end
46
+
47
+
48
+ def show_debug
49
+ ENV['SHOWSOAP'] == 'true'
50
+ end
51
+
52
+
53
+ def init_server(url)
54
+ @url = URI.parse(url)
55
+ @server = Net::HTTP.new(@url.host, @url.port)
56
+ @server.use_ssl = @url.scheme == 'https'
57
+ @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
58
+
59
+ # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
60
+ @server.set_debug_output $stderr if show_debug
61
+ end
62
+
63
+
64
+ # Log in to the server and remember the session ID
65
+ # returned to us by SalesForce.
66
+ def login(user, password)
67
+ @user = user
68
+ @password = password
69
+
70
+ response = call_remote(:login, [:username, user, :password, password])
71
+
72
+ raise "Incorrect user name / password [#{response[:Fault][:faultstring]}]" unless response.loginResponse
73
+
74
+ result = response[:loginResponse][:result]
75
+ @session_id = result[:sessionId]
76
+
77
+ init_server(result[:serverUrl])
78
+
79
+ response
80
+ end
81
+
82
+
83
+ # Call a method on the remote server. Arguments can be
84
+ # a hash or (if order is important) an array of alternating
85
+ # keys and values.
86
+ def call_remote(method, args)
87
+ # Create XML text from the arguments.
88
+ expanded = ''
89
+ @builder = Builder::XmlMarkup.new(:target => expanded)
90
+ expand(@builder, {method => args}, 'urn:partner.soap.sforce.com')
91
+
92
+ extra_headers = ""
93
+ extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
94
+ extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
95
+ extra_headers << MruHeader if update_mru
96
+ extra_headers << (ClientIdHeader % client_id) if client_id
97
+
98
+ if trigger_user_email or trigger_other_email or trigger_auto_response_email
99
+ extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
100
+
101
+ extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
102
+ extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
103
+ extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
104
+
105
+ extra_headers << '</partner:EmailHeader>'
106
+ end
107
+
108
+ # Fill in the blanks of the SOAP envelope with our
109
+ # session ID and the expanded XML of our request.
110
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
111
+
112
+ # reset the batch size for the next request
113
+ @batch_size = DEFAULT_BATCH_SIZE
114
+
115
+ # gzip request
116
+ request = encode(request)
117
+
118
+ headers = {
119
+ 'Connection' => 'Keep-Alive',
120
+ 'Content-Type' => 'text/xml',
121
+ 'SOAPAction' => '""',
122
+ 'User-Agent' => 'activesalesforce rforce/1.0'
123
+ }
124
+
125
+ unless show_debug
126
+ headers['Accept-Encoding'] = 'gzip'
127
+ headers['Content-Encoding'] = 'gzip'
128
+ end
129
+
130
+ # Send the request to the server and read the response.
131
+ response = @server.post2(@url.path, request.lstrip, headers)
132
+
133
+ # decode if we have encoding
134
+ content = decode(response)
135
+
136
+ # Check to see if INVALID_SESSION_ID was raised and try to relogin in
137
+ if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
138
+ login(@user, @password)
139
+
140
+ # repackage and rencode request with the new session id
141
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
142
+ request = encode(request)
143
+
144
+ # Send the request to the server and read the response.
145
+ response = @server.post2(@url.path, request.lstrip, headers)
146
+
147
+ content = decode(response)
148
+ end
149
+
150
+ SoapResponse.new(content).parse
151
+ end
152
+
153
+
154
+ # decode gzip
155
+ def decode(response)
156
+ encoding = response['Content-Encoding']
157
+
158
+ # return body if no encoding
159
+ if !encoding then return response.body end
160
+
161
+ # decode gzip
162
+ case encoding.strip
163
+ when 'gzip':
164
+ begin
165
+ gzr = Zlib::GzipReader.new(StringIO.new(response.body))
166
+ decoded = gzr.read
167
+ ensure
168
+ gzr.close
169
+ end
170
+ decoded
171
+ else
172
+ response.body
173
+ end
174
+ end
175
+
176
+
177
+ # encode gzip
178
+ def encode(request)
179
+ return request if show_debug
180
+
181
+ begin
182
+ ostream = StringIO.new
183
+ gzw = Zlib::GzipWriter.new(ostream)
184
+ gzw.write(request)
185
+ ostream.string
186
+ ensure
187
+ gzw.close
188
+ end
189
+ end
190
+
191
+
192
+ # Turns method calls on this object into remote SOAP calls.
193
+ def method_missing(method, *args)
194
+ unless args.size == 1 && [Hash, Array].include?(args[0].class)
195
+ raise 'Expected 1 Hash or Array argument'
196
+ end
197
+
198
+ call_remote method, args[0]
199
+ end
200
+ end
201
+ end
202
+
@@ -0,0 +1,14 @@
1
+ module RForce
2
+ # Allows indexing hashes like method calls: hash.key
3
+ # to supplement the traditional way of indexing: hash[key]
4
+ module MethodKeys
5
+ def method_missing(method, *args)
6
+ raise NoMethodError unless respond_to?('[]')
7
+ self[method]
8
+ end
9
+ end
10
+
11
+ class MethodHash < Hash
12
+ include MethodKeys
13
+ end
14
+ end
@@ -0,0 +1,93 @@
1
+ module RForce
2
+ module SoapPullable
3
+ SOAP_ENVELOPE = 'soapenv:Envelope'
4
+
5
+ # Split off the local name portion of an XML tag.
6
+ def local(tag)
7
+ first, second = tag.split ':'
8
+ return first if second.nil?
9
+ @namespaces.include?(first) ? second : tag
10
+ end
11
+
12
+ def tag_start(name, attrs)
13
+ tag_name = local name
14
+
15
+ # For shorter hash keys, we can strip any namespaces of the SOAP
16
+ # envelope tag from the tags inside it.
17
+ if name == SOAP_ENVELOPE
18
+ @namespaces = attrs.keys.grep(/xmlns:/).map {|k| k.split(':').last}
19
+ return
20
+ end
21
+
22
+ @stack.push OpenHash.new({})
23
+ end
24
+
25
+ def text(data)
26
+ unless data.nil? || data.strip.empty?
27
+ @current_value = "" if @current_value.nil?
28
+ @current_value << data
29
+ end
30
+ end
31
+
32
+ def tag_end(name)
33
+ return if @done
34
+
35
+ tag_name = local name
36
+
37
+ return if tag_name == SOAP_ENVELOPE
38
+
39
+ working_hash = @stack.pop
40
+
41
+ # We are either done or working on a certain depth in the current
42
+ # stack.
43
+ if @stack.empty?
44
+ @parsed = working_hash
45
+ @done = true
46
+ return
47
+ else
48
+ index = @stack.size - 1
49
+ end
50
+
51
+ # working_hash and @current_value have a mutually exclusive relationship.
52
+ # If the current element doesn't have a value then it means that there
53
+ # is a nested data structure. In this case then working_hash is populated
54
+ # and @current_value is nil. Conversely, if @current_value has a value
55
+ # then we do not have a nested data sctructure and working_hash will
56
+ # be empty.
57
+ use_value = working_hash.empty? ? @current_value : working_hash
58
+ tag_sym = tag_name.to_sym
59
+ element = @stack[index][tag_sym]
60
+
61
+ if @stack[index].keys.include? tag_sym
62
+ # This is here to handle the Id value being included twice and thus
63
+ # producing an array. We skip the second instance so the array is
64
+ # not created.
65
+ #
66
+ # We also need to clear out the current value, so that the next
67
+ # tag doesn't erroneously pick up the value of the Id.
68
+ if tag_name == 'Id'
69
+ @current_value = nil
70
+ return
71
+ end
72
+
73
+ # We are here because the name of our current element is one that
74
+ # already exists in the hash. If this is the first encounter with
75
+ # the duplicate tag_name then we convert the existing value to an
76
+ # array otherwise we push the value we are working with and add it
77
+ # to the existing array.
78
+ if element.is_a?( Array )
79
+ element << use_value
80
+ else
81
+ @stack[index][tag_sym] = [element, use_value]
82
+ end
83
+ else
84
+ # We are here because the name of our current element has not been
85
+ # assigned yet.
86
+ @stack[index][tag_sym] = use_value
87
+ end
88
+
89
+ # We are done with the current tag so reset the data for the next one
90
+ @current_value = nil
91
+ end
92
+ end
93
+ end