DerGuteMoritz-rforce 0.4.2

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 (51) hide show
  1. data/.hgignore +3 -0
  2. data/.hgtags +6 -0
  3. data/.specification +115 -0
  4. data/DerGuteMoritz-rforce.gemspec +103 -0
  5. data/History.txt +29 -0
  6. data/LICENSE +20 -0
  7. data/README.rdoc +52 -0
  8. data/Rakefile +56 -0
  9. data/VERSION +1 -0
  10. data/bugs/issue-09d998dc3f9d6e8928598fd5d9c6627102dedd65.yaml +18 -0
  11. data/bugs/issue-1b668248120eb4fd055f0ea6b75ff807f6955936.yaml +22 -0
  12. data/bugs/issue-3ef7a7c5c7e38fb76e1f53e1b67f45559e21b104.yaml +18 -0
  13. data/bugs/issue-a7a87cb98d29943a49d1608adb68475838ad2e65.yaml +22 -0
  14. data/bugs/issue-c92d34e10038ba6b9acfd7e3dfb2b154e463baba.yaml +29 -0
  15. data/bugs/project.yaml +16 -0
  16. data/lib/rforce.rb +88 -0
  17. data/lib/rforce/binding.rb +261 -0
  18. data/lib/rforce/method_keys.rb +14 -0
  19. data/lib/rforce/soap_pullable.rb +93 -0
  20. data/lib/rforce/soap_response.rb +7 -0
  21. data/lib/rforce/soap_response_expat.rb +35 -0
  22. data/lib/rforce/soap_response_hpricot.rb +75 -0
  23. data/lib/rforce/soap_response_nokogiri.rb +64 -0
  24. data/lib/rforce/soap_response_rexml.rb +34 -0
  25. data/lib/rforce/version.rb +3 -0
  26. data/rforce.tmproj +122 -0
  27. data/spec/rforce_spec.rb +105 -0
  28. data/spec/soap-response.xml +5928 -0
  29. data/spec/spec.opts +1 -0
  30. data/spec/spec_helper.rb +66 -0
  31. data/tasks/timing.rake +21 -0
  32. data/test/oauth_tests.rb +42 -0
  33. data/wsdl/README.html +23 -0
  34. data/wsdl/README.txt +22 -0
  35. data/wsdl/ca.pem +36 -0
  36. data/wsdl/default.rb +1627 -0
  37. data/wsdl/defaultDriver.rb +179 -0
  38. data/wsdl/orderedhash.rb +254 -0
  39. data/wsdl/partner.wsdl.xml +3104 -0
  40. data/wsdl/partner.wsdl.xml.old +1858 -0
  41. data/wsdl/sample/create.rb +37 -0
  42. data/wsdl/sample/describeGlobal.rb +28 -0
  43. data/wsdl/sample/describeSObjects.rb +34 -0
  44. data/wsdl/sample/getServerTimestamp.rb +27 -0
  45. data/wsdl/sample/query.rb +31 -0
  46. data/wsdl/sample/queryMore.rb +32 -0
  47. data/wsdl/sample/retrieve.rb +32 -0
  48. data/wsdl/sforceDriver.rb +43 -0
  49. data/wsdl/soap/property +1 -0
  50. data/wsdl/wsdl.rb +121 -0
  51. metadata +182 -0
@@ -0,0 +1,22 @@
1
+ --- !ditz.rubyforge.org,2008-03-06/issue
2
+ title: Binding#call_remote should use parse()
3
+ desc: Search for uses of the old API (SoapResponse#new) and replace with the new API (SoapResponse#parse).
4
+ type: :bugfix
5
+ component: rforce
6
+ release: "0.3"
7
+ reporter: Ian Dees <undees@gmail.com>
8
+ status: :closed
9
+ disposition: :fixed
10
+ creation_time: 2008-07-28 05:37:21.978938 Z
11
+ references: []
12
+
13
+ id: a7a87cb98d29943a49d1608adb68475838ad2e65
14
+ log_events:
15
+ - - 2008-07-28 05:37:24.755692 Z
16
+ - Ian Dees <undees@gmail.com>
17
+ - created
18
+ - ""
19
+ - - 2008-07-28 05:39:29.817686 Z
20
+ - Ian Dees <undees@gmail.com>
21
+ - closed issue with disposition fixed
22
+ - Done.
@@ -0,0 +1,29 @@
1
+ --- !ditz.rubyforge.org,2008-03-06/issue
2
+ title: Select XML parser at runtime
3
+ desc: |-
4
+ XML parsing is done by three different classes: SoapResponse, SoapResponseHpricot, and SoapResponseExpat. The Binding class, in a holdover from the old days, is hard-coded to use SoapResponse.
5
+
6
+ At the very least, we should scan the installed libraries / gems and choose the fastest one available. We might also allow Binding's initializer to pass in a specific parser.
7
+ type: :task
8
+ component: rforce
9
+ release: "0.3"
10
+ reporter: Ian Dees <undees@gmail.com>
11
+ status: :closed
12
+ disposition: :fixed
13
+ creation_time: 2008-07-23 17:10:22.283059 Z
14
+ references: []
15
+
16
+ id: c92d34e10038ba6b9acfd7e3dfb2b154e463baba
17
+ log_events:
18
+ - - 2008-07-23 17:10:29.352269 Z
19
+ - Ian Dees <undees@gmail.com>
20
+ - created
21
+ - ""
22
+ - - 2008-07-28 05:37:48.378891 Z
23
+ - Ian Dees <undees@gmail.com>
24
+ - assigned to release 0.3 from unassigned
25
+ - ""
26
+ - - 2008-07-28 05:39:05.294916 Z
27
+ - Ian Dees <undees@gmail.com>
28
+ - closed issue with disposition fixed
29
+ - Used the simple fix of seeing which constants are defined and assigning SoapResponse based on that.
data/bugs/project.yaml ADDED
@@ -0,0 +1,16 @@
1
+ --- !ditz.rubyforge.org,2008-03-06/project
2
+ name: rforce
3
+ version: "0.3"
4
+ components:
5
+ - !ditz.rubyforge.org,2008-03-06/component
6
+ name: rforce
7
+ releases:
8
+ - !ditz.rubyforge.org,2008-03-06/release
9
+ name: "0.3"
10
+ status: :unreleased
11
+ release_time:
12
+ log_events:
13
+ - - 2008-07-28 05:35:54.153742 Z
14
+ - Ian Dees <undees@gmail.com>
15
+ - created
16
+ - Will be ready for ActiveSalesforce.
data/lib/rforce.rb ADDED
@@ -0,0 +1,88 @@
1
+ =begin
2
+ Copyright (c) 2005-2010 Ian Dees and contributors
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ =end
22
+
23
+ # RForce is a simple Ruby binding to the SalesForce CRM system.
24
+ # Rather than enforcing adherence to the sforce.com schema,
25
+ # RForce assumes you are familiar with the API. Ruby method names
26
+ # become SOAP method names. Nested Ruby hashes become nested
27
+ # XML elements.
28
+ #
29
+ # Example:
30
+ #
31
+ # binding = RForce::Binding.new 'https://www.salesforce.com/services/Soap/u/10.0'
32
+ # binding.login 'username', 'password'
33
+ # answer = binding.search(
34
+ # :searchString =>
35
+ # 'find {Some Account Name} in name fields returning account(id)')
36
+ # account_id = answer.searchResponse.result.searchRecords.record.Id
37
+ #
38
+ # opportunity = {
39
+ # :accountId => account_id,
40
+ # :amount => "10.00",
41
+ # :name => "New sale",
42
+ # :closeDate => "2005-09-01",
43
+ # :stageName => "Closed Won"
44
+ # }
45
+ #
46
+ # binding.create 'sObject {"xsi:type" => "Opportunity"}' => opportunity
47
+ #
48
+
49
+ require 'rforce/binding'
50
+ require 'rforce/soap_response'
51
+
52
+
53
+ module RForce
54
+ # Expand Ruby data structures into XML.
55
+ def expand(builder, args, xmlns = nil)
56
+ # Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
57
+ if (args.class == Array)
58
+ args.each_index{|i| args[i, 2] = [args[i, 2]]}
59
+ end
60
+
61
+ args.each do |key, value|
62
+ attributes = xmlns ? {:xmlns => xmlns} : {}
63
+
64
+ # If the XML tag requires attributes,
65
+ # the tag name will contain a space
66
+ # followed by a string representation
67
+ # of a hash of attributes.
68
+ #
69
+ # e.g. 'sObject {"xsi:type" => "Opportunity"}'
70
+ # becomes <sObject xsi:type="Opportunity>...</sObject>
71
+ if key.is_a? String
72
+ key, modifier = key.split(' ', 2)
73
+
74
+ attributes.merge!(eval(modifier)) if modifier
75
+ end
76
+
77
+ # Create an XML element and fill it with this
78
+ # value's sub-items.
79
+ case value
80
+ when Hash, Array then
81
+ builder.tag!(key, attributes) do expand builder, value; end
82
+
83
+ when String then
84
+ builder.tag!(key, attributes) { builder.text! value }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,261 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+ require 'zlib'
4
+ require 'stringio'
5
+ require 'rexml/document'
6
+ require 'builder'
7
+ require 'oauth' # the 0.3.6 version must be checked out
8
+
9
+
10
+ module RForce
11
+ # Implements the connection to the SalesForce server.
12
+ class Binding
13
+ include RForce
14
+
15
+ DEFAULT_BATCH_SIZE = 2000
16
+ attr_accessor :batch_size, :url, :assignment_rule_id,
17
+ :use_default_rule, :update_mru, :client_id,
18
+ :trigger_user_email,
19
+ :trigger_other_email, :trigger_auto_response_email
20
+
21
+ # Fill in the guts of this typical SOAP envelope
22
+ # with the session ID and the body of the SOAP request.
23
+ Envelope = <<-HERE
24
+ <?xml version="1.0" encoding="utf-8" ?>
25
+ <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
26
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
27
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
28
+ xmlns:partner="urn:partner.soap.sforce.com">
29
+ xmlns:spartner="urn:sobject.partner.soap.sforce.com">
30
+ <soap:Header>
31
+ <partner:SessionHeader soap:mustUnderstand='1'>
32
+ <partner:sessionId>%s</partner:sessionId>
33
+ </partner:SessionHeader>
34
+ <partner:QueryOptions soap:mustUnderstand='1'>
35
+ <partner:batchSize>%d</partner:batchSize>
36
+ </partner:QueryOptions>
37
+ %s
38
+ </soap:Header>
39
+ <soap:Body>
40
+ %s
41
+ </soap:Body>
42
+ </soap:Envelope>
43
+ HERE
44
+
45
+ AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
46
+ AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
47
+ MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
48
+ ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
49
+
50
+ # Connect to the server securely. If you pass an oauth hash, it
51
+ # must contain the keys :consumer_key, :consumer_secret,
52
+ # :access_token, :access_secret, and :login_url.
53
+ def initialize(url, sid = nil, oauth = nil)
54
+ @session_id = sid
55
+ @oauth = oauth
56
+ @batch_size = DEFAULT_BATCH_SIZE
57
+
58
+ init_server(url)
59
+ end
60
+
61
+
62
+ def show_debug
63
+ ENV['SHOWSOAP'] == 'true'
64
+ end
65
+
66
+
67
+ def init_server(url)
68
+ @url = URI.parse(url)
69
+ @server = Net::HTTP.new(@url.host, @url.port)
70
+ @server.use_ssl = @url.scheme == 'https'
71
+ @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
72
+
73
+ # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
74
+ @server.set_debug_output $stderr if show_debug
75
+ end
76
+
77
+
78
+ def consumer
79
+ OAuth::Consumer.new(@oauth[:consumer_key],
80
+ @oauth[:consumer_secret], {
81
+ :site => "https://login.salesforce.com",
82
+ :signature_method => 'HMAC-SHA1', # this is default, but just for clarity
83
+ :scheme => :body,
84
+ :request_token_path => "/_nc_external/system/security/oauth/RequestTokenHandler",
85
+ :access_token_path => "/_nc_external/system/security/oauth/AccessTokenHandler",
86
+ :authorize_path => '/setup/secur/RemoteAccessAuthorizationPage.apexp',
87
+ })
88
+ end
89
+
90
+
91
+
92
+ # Log in to the server with OAuth, remembering
93
+ # the session ID returned to us by SalesForce.
94
+ def login_with_oauth
95
+ # post is method of Oauth::AccessToken, @server is instance of it
96
+ access_token = OAuth::AccessToken.new consumer, @oauth[:access_token], @oauth[:access_secret]
97
+ result = access_token.post @oauth[:login_url]
98
+
99
+ case result
100
+ when Net::HTTPSuccess
101
+ doc = REXML::Document.new result.body
102
+ @session_id = doc.elements['*/sessionId'].text
103
+ server_url = doc.elements['*/serverUrl'].text
104
+ init_server(server_url)
105
+
106
+ return {:sessionId => @session_id, :serverUrl => server_url}
107
+ when Net::HTTPUnauthorized
108
+ raise "Invalid OAuth tokens=#{@oauth.inspect}"
109
+ else
110
+ raise "Unexpected error: #{result.inspect}"
111
+ end
112
+ end
113
+
114
+
115
+
116
+
117
+ # Log in to the server with a user name and password, remembering
118
+ # the session ID returned to us by SalesForce.
119
+ def login(user, password)
120
+ @user = user
121
+ @password = password
122
+
123
+ response = call_remote(:login, [:username, user, :password, password])
124
+
125
+ raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
126
+
127
+ result = response[:loginResponse][:result]
128
+ @session_id = result[:sessionId]
129
+
130
+ init_server(result[:serverUrl])
131
+
132
+ response
133
+ end
134
+
135
+
136
+
137
+ # Call a method on the remote server. Arguments can be
138
+ # a hash or (if order is important) an array of alternating
139
+ # keys and values.
140
+ def call_remote(method, args)
141
+
142
+ urn, soap_url = block_given? ? yield : ["urn:partner.soap.sforce.com", @url.path]
143
+
144
+ # Create XML text from the arguments.
145
+ expanded = ''
146
+ @builder = Builder::XmlMarkup.new(:target => expanded)
147
+ expand(@builder, {method => args}, urn)
148
+
149
+ extra_headers = ""
150
+ extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
151
+ extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
152
+ extra_headers << MruHeader if update_mru
153
+ extra_headers << (ClientIdHeader % client_id) if client_id
154
+
155
+ if trigger_user_email or trigger_other_email or trigger_auto_response_email
156
+ extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
157
+
158
+ extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
159
+ extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
160
+ extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
161
+
162
+ extra_headers << '</partner:EmailHeader>'
163
+ end
164
+
165
+ # Fill in the blanks of the SOAP envelope with our
166
+ # session ID and the expanded XML of our request.
167
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
168
+
169
+ # reset the batch size for the next request
170
+ @batch_size = DEFAULT_BATCH_SIZE
171
+
172
+ # gzip request
173
+ request = encode(request)
174
+
175
+ headers = {
176
+ 'Connection' => 'Keep-Alive',
177
+ 'Content-Type' => 'text/xml',
178
+ 'SOAPAction' => '""',
179
+ 'User-Agent' => 'activesalesforce rforce/0.4.1 LH'
180
+ }
181
+
182
+ unless show_debug
183
+ headers['Accept-Encoding'] = 'gzip'
184
+ headers['Content-Encoding'] = 'gzip'
185
+ end
186
+
187
+ # Send the request to the server and read the response.
188
+ response = @server.post2(soap_url, request.lstrip, headers)
189
+
190
+ # decode if we have encoding
191
+ content = decode(response)
192
+
193
+ # Check to see if INVALID_SESSION_ID was raised and try to relogin in
194
+ if method != :login && @session_id && content =~ /sf:INVALID_SESSION_ID/
195
+ if @oauth
196
+ login_with_oauth
197
+ else
198
+ login(@user, @password)
199
+ end
200
+
201
+ # repackage and rencode request with the new session id
202
+ request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
203
+ request = encode(request)
204
+
205
+ # Send the request to the server and read the response.
206
+ response = @server.post2(soap_url, request.lstrip, headers)
207
+
208
+ content = decode(response)
209
+ end
210
+
211
+ SoapResponse.new(content).parse
212
+ end
213
+
214
+
215
+ # decode gzip
216
+ def decode(response)
217
+ encoding = response['Content-Encoding']
218
+
219
+ # return body if no encoding
220
+ if !encoding then return response.body end
221
+
222
+ # decode gzip
223
+ case encoding.strip
224
+ when 'gzip' then
225
+ begin
226
+ gzr = Zlib::GzipReader.new(StringIO.new(response.body))
227
+ decoded = gzr.read
228
+ ensure
229
+ gzr.close
230
+ end
231
+ decoded
232
+ else
233
+ response.body
234
+ end
235
+ end
236
+
237
+
238
+ # encode gzip
239
+ def encode(request)
240
+ return request if show_debug
241
+ begin
242
+ ostream = StringIO.new
243
+ gzw = Zlib::GzipWriter.new(ostream)
244
+ gzw.write(request)
245
+ ostream.string
246
+ ensure
247
+ gzw.close
248
+ end
249
+ end
250
+
251
+
252
+ # Turns method calls on this object into remote SOAP calls.
253
+ def method_missing(method, *args)
254
+ unless args.size == 1 && [Hash, Array].include?(args[0].class)
255
+ raise 'Expected 1 Hash or Array argument'
256
+ end
257
+
258
+ call_remote method, args[0]
259
+ end
260
+ end
261
+ end
@@ -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
+ require 'rforce/method_keys'
2
+
3
+ module RForce
4
+ module SoapPullable
5
+ SOAP_ENVELOPE = 'soapenv:Envelope'
6
+
7
+ # Split off the local name portion of an XML tag.
8
+ def local(tag)
9
+ first, second = tag.split ':'
10
+ return first if second.nil?
11
+ @namespaces.include?(first) ? second : tag
12
+ end
13
+
14
+ def tag_start(name, attrs)
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 MethodHash.new
23
+ end
24
+
25
+ def text(data)
26
+ adding = data.strip.empty? ? nil : data
27
+
28
+ if adding
29
+ @current_value = (@current_value || '') + adding
30
+ end
31
+ end
32
+
33
+ def tag_end(name)
34
+ return if @done || name == SOAP_ENVELOPE
35
+
36
+ tag_name = local name
37
+ working_hash = @stack.pop
38
+
39
+ # We are either done or working on a certain depth in the current
40
+ # stack.
41
+ if @stack.empty?
42
+ @parsed = working_hash
43
+ @done = true
44
+ return
45
+ end
46
+
47
+ index = @stack.size - 1
48
+
49
+ # working_hash and @current_value have a mutually exclusive relationship.
50
+ # If the current element doesn't have a value then it means that there
51
+ # is a nested data structure. In this case then working_hash is populated
52
+ # and @current_value is nil. Conversely, if @current_value has a value
53
+ # then we do not have a nested data structure and working_hash will
54
+ # be empty.
55
+ raise 'Parser is confused' unless working_hash.empty? || @current_value.nil?
56
+
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