sportgenic-activerecord-activesalesforce-adapter 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) 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 +823 -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/sid_authentication_filter.rb +57 -0
  13. data/lib/rforce/binding.rb +202 -0
  14. data/lib/rforce/method_keys.rb +14 -0
  15. data/lib/rforce/soap_pullable.rb +90 -0
  16. data/lib/rforce/soap_response_expat.rb +35 -0
  17. data/lib/rforce/soap_response_hpricot.rb +70 -0
  18. data/lib/rforce/soap_response_rexml.rb +34 -0
  19. data/lib/rforce/version.rb +3 -0
  20. data/lib/rforce.rb +116 -0
  21. data/test/unit/basic_test.rb +203 -0
  22. data/test/unit/config.yml +5 -0
  23. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_add_notes_to_contact.recording +1966 -0
  24. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_assignment_rule_id.recording +1621 -0
  25. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_batch_insert.recording +1611 -0
  26. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_client_id.recording +1618 -0
  27. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_count_contacts.recording +1620 -0
  28. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_create_a_contact.recording +1611 -0
  29. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact.recording +1611 -0
  30. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_first_name.recording +3468 -0
  31. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_a_contact_by_id.recording +1664 -0
  32. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_find_addresses.recording +1635 -0
  33. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_get_created_by_from_contact.recording +4307 -0
  34. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_master_detail.recording +1951 -0
  35. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_read_all_content_columns.recording +1611 -0
  36. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_save_a_contact.recording +1611 -0
  37. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_default_rule.recording +1618 -0
  38. data/test/unit/recorded_results/AsfUnitTestsBasicTest.test_use_update_mru.recording +1618 -0
  39. data/test/unit/recorded_test_case.rb +83 -0
  40. metadata +160 -0
@@ -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,90 @@
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
+ @current_value = data.strip.empty? ? nil : data
27
+ end
28
+
29
+ def tag_end(name)
30
+ return if @done
31
+
32
+ tag_name = local name
33
+
34
+ return if tag_name == SOAP_ENVELOPE
35
+
36
+ working_hash = @stack.pop
37
+
38
+ # We are either done or working on a certain depth in the current
39
+ # stack.
40
+ if @stack.empty?
41
+ @parsed = working_hash
42
+ @done = true
43
+ return
44
+ else
45
+ index = @stack.size - 1
46
+ end
47
+
48
+ # working_hash and @current_value have a mutually exclusive relationship.
49
+ # If the current element doesn't have a value then it means that there
50
+ # is a nested data structure. In this case then working_hash is populated
51
+ # and @current_value is nil. Conversely, if @current_value has a value
52
+ # then we do not have a nested data sctructure and working_hash will
53
+ # be empty.
54
+ use_value = working_hash.empty? ? @current_value : working_hash
55
+ tag_sym = tag_name.to_sym
56
+ element = @stack[index][tag_sym]
57
+
58
+ if @stack[index].keys.include? tag_sym
59
+ # This is here to handle the Id value being included twice and thus
60
+ # producing an array. We skip the second instance so the array is
61
+ # not created.
62
+ #
63
+ # We also need to clear out the current value, so that the next
64
+ # tag doesn't erroneously pick up the value of the Id.
65
+ if tag_name == 'Id'
66
+ @current_value = nil
67
+ return
68
+ end
69
+
70
+ # We are here because the name of our current element is one that
71
+ # already exists in the hash. If this is the first encounter with
72
+ # the duplicate tag_name then we convert the existing value to an
73
+ # array otherwise we push the value we are working with and add it
74
+ # to the existing array.
75
+ if element.is_a?( Array )
76
+ element << use_value
77
+ else
78
+ @stack[index][tag_sym] = [element, use_value]
79
+ end
80
+ else
81
+ # We are here because the name of our current element has not been
82
+ # assigned yet.
83
+ @stack[index][tag_sym] = use_value
84
+ end
85
+
86
+ # We are done with the current tag so reset the data for the next one
87
+ @current_value = nil
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ require 'xml/parser'
2
+ require 'rforce/soap_pullable'
3
+
4
+ module RForce
5
+ class SoapResponseExpat
6
+ include SoapPullable
7
+
8
+ def initialize(content)
9
+ @content = content
10
+ end
11
+
12
+ def parse
13
+ @current_value = nil
14
+ @stack = []
15
+ @parsed = OpenHash.new({})
16
+ @done = false
17
+ @namespaces = []
18
+
19
+ XML::Parser.new.parse(@content) do |type, name, data|
20
+ case type
21
+ when XML::Parser::START_ELEM
22
+ tag_start name, data
23
+ when XML::Parser::CDATA
24
+ text data
25
+ when XML::Parser::END_ELEM
26
+ tag_end name
27
+ end
28
+
29
+ break if @done
30
+ end
31
+
32
+ @parsed
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,70 @@
1
+ require 'hpricot'
2
+
3
+
4
+ module RForce
5
+ class SoapResponseHpricot
6
+ # Parses an XML string into structured data.
7
+ def initialize(content)
8
+ @content = content
9
+ end
10
+
11
+ # Digests an XML DOM node into nested Ruby types.
12
+ def parse
13
+ document = Hpricot.XML(@content)
14
+ node = document % 'soapenv:Body'
15
+ self.class.node_to_ruby node
16
+ end
17
+
18
+ private
19
+
20
+ def self.node_to_ruby(node)
21
+ # Convert text nodes into simple strings.
22
+ children = node.children.reject do |c|
23
+ c.is_a?(Hpricot::Text) && c.to_s.strip.empty?
24
+ end
25
+
26
+ if node.is_a?(Hpricot::Text)
27
+ return node.inner_text
28
+ end
29
+
30
+ if children.first.is_a?(Hpricot::Text)
31
+ return children.first
32
+ end
33
+
34
+ # Convert nodes with children into MethodHashes.
35
+ elements = OpenHash.new({})
36
+
37
+ # Add all the element's children to the hash.
38
+ children.each do |e|
39
+ next if e.is_a?(Hpricot::Text) && e.to_s.strip.empty?
40
+ name = e.name
41
+
42
+ if name.include? ':'
43
+ name = name.split(':').last
44
+ end
45
+
46
+ name = name.to_sym
47
+
48
+ case elements[name]
49
+ # The most common case: unique child element tags.
50
+ when NilClass: elements[name] = node_to_ruby(e)
51
+
52
+ # Non-unique child elements become arrays:
53
+
54
+ # We've already created the array: just
55
+ # add the element.
56
+ when Array: elements[name] << node_to_ruby(e)
57
+
58
+ # We haven't created the array yet: do so,
59
+ # then put the existing element in, followed
60
+ # by the new one.
61
+ else
62
+ elements[name] = [elements[name]]
63
+ elements[name] << node_to_ruby(e)
64
+ end
65
+ end
66
+
67
+ return elements.empty? ? nil : elements
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,34 @@
1
+ require 'rexml/document'
2
+ require 'rexml/xpath'
3
+ require 'rforce/soap_pullable'
4
+
5
+
6
+ module RForce
7
+ # Turns an XML response from the server into a Ruby
8
+ # object whose methods correspond to nested XML elements.
9
+ class SoapResponseRexml
10
+ include SoapPullable
11
+
12
+ %w(attlistdecl cdata comment doctype doctype_end elementdecl
13
+ entity entitydecl instruction notationdecl xmldecl).each do |unused|
14
+ define_method(unused) {}
15
+ end
16
+
17
+ def initialize(content)
18
+ @content = content
19
+ end
20
+
21
+ # Parses an XML string into structured data.
22
+ def parse
23
+ @current_value = nil
24
+ @stack = []
25
+ @parsed = OpenHash.new({})
26
+ @done = false
27
+ @namespaces = []
28
+
29
+ REXML::Document.parse_stream @content, self
30
+
31
+ @parsed
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module RForce
2
+ VERSION = '0.3'
3
+ end
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