DerGuteMoritz-rforce 0.4.2

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