althor880-activerecord-activesalesforce-adapter 2.3.5

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