rforce 0.2.1 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,3 +12,8 @@
12
12
 
13
13
  * 1 minor enhancement:
14
14
  * Updated examples for SalesForce API v10
15
+
16
+ == 0.3 2009-04-16
17
+
18
+ * 1 minor enhancement:
19
+ * Updated for Ruby 1.9
@@ -3,7 +3,15 @@ Manifest.txt
3
3
  README.txt
4
4
  Rakefile
5
5
  lib/rforce.rb
6
+ lib/rforce/binding.rb
7
+ lib/rforce/soap_pullable.rb
8
+ lib/rforce/soap_response_expat.rb
9
+ lib/rforce/soap_response_hpricot.rb
10
+ lib/rforce/soap_response_rexml.rb
11
+ lib/rforce/version.rb
6
12
  spec/rforce_spec.rb
13
+ spec/soap-response.xml
7
14
  spec/spec.opts
8
15
  spec/spec_helper.rb
9
16
  tasks/rspec.rake
17
+ tasks/timing.rake
data/README.txt CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  * http://rforce.rubyforge.org
4
4
  * http://rubyforge.org/projects/rforce
5
- * http://freehg.org/u/undees/rforce
5
+ * http://bitbucket.org/undees/rforce
6
+ * http://github.com/undees/rforce
6
7
 
7
8
  == DESCRIPTION:
8
9
 
@@ -15,7 +16,7 @@ Rather than enforcing adherence to the sforce.com schema, RForce assumes you are
15
16
  == SYNOPSIS:
16
17
 
17
18
  binding = RForce::Binding.new \
18
- 'https://na2.salesforce.com/services/Soap/u/10.0'
19
+ 'https://www.salesforce.com/services/Soap/u/10.0'
19
20
 
20
21
  binding.login \
21
22
  'email', 'password_with_token'
@@ -52,7 +53,7 @@ Rather than enforcing adherence to the sforce.com schema, RForce assumes you are
52
53
 
53
54
  == LICENSE:
54
55
 
55
- Copyright (c) 2005-2008 Ian Dees
56
+ Copyright (c) 2005-2009 Ian Dees and contributors
56
57
 
57
58
  Permission is hereby granted, free of charge, to any person obtaining
58
59
  a copy of this software and associated documentation files (the
@@ -71,4 +72,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
71
72
  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
72
73
  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
73
74
  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
74
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
75
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,12 +1,13 @@
1
1
  # -*- ruby -*-
2
2
 
3
+ $:.unshift './lib'
3
4
  require 'rubygems'
4
5
  require 'hoe'
5
- require './lib/rforce.rb'
6
+ require 'rforce/version'
6
7
 
7
8
  Hoe.new('rforce', RForce::VERSION) do |p|
8
9
  p.developer('Ian Dees', 'undees@gmail.com')
9
- p.extra_deps = [['builder', '>= 2.0.0']]
10
+ p.extra_deps = [['builder', '>= 2.0.0'], ['facets', '>= 2.4']]
10
11
  p.extra_dev_deps = [['hoe', '>= 1.7.0']]
11
12
  p.remote_rdoc_dir = ''
12
13
  end
@@ -1,6 +1,5 @@
1
1
  =begin
2
- RForce v0.2
3
- Copyright (c) 2005-2008 Ian Dees
2
+ Copyright (c) 2005-2009 Ian Dees and contributors
4
3
 
5
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
5
  of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +28,7 @@ SOFTWARE.
29
28
  #
30
29
  # Example:
31
30
  #
32
- # binding = RForce::Binding.new 'na1-api.salesforce.com'
31
+ # binding = RForce::Binding.new 'https://www.salesforce.com/services/Soap/u/10.0'
33
32
  # binding.login 'username', 'password'
34
33
  # answer = binding.search(
35
34
  # :searchString =>
@@ -52,83 +51,35 @@ require 'net/https'
52
51
  require 'uri'
53
52
  require 'zlib'
54
53
  require 'stringio'
55
- require 'rexml/document'
56
- require 'rexml/xpath'
54
+
57
55
  require 'rubygems'
56
+
58
57
  gem 'builder', '>= 2.0.0'
59
58
  require 'builder'
60
59
 
60
+ gem 'facets', '>= 2.4'
61
+ require 'facets/openhash'
61
62
 
62
- module RForce
63
- VERSION = '0.2.1'
64
-
65
- #Allows indexing hashes like method calls: hash.key
66
- #to supplement the traditional way of indexing: hash[key]
67
- module FlashHash
68
- def method_missing(method, *args)
69
- self[method]
70
- end
71
- end
72
-
73
- #Turns an XML response from the server into a Ruby
74
- #object whose methods correspond to nested XML elements.
75
- class SoapResponse
76
- include FlashHash
77
-
78
- #Parses an XML string into structured data.
79
- def initialize(content)
80
- document = REXML::Document.new content
81
- node = REXML::XPath.first document, '//soapenv:Body'
82
- @parsed = SoapResponse.parse node
83
- end
84
-
85
- #Allows this object to act like a hash (and therefore
86
- #as a FlashHash via the include above).
87
- def [](symbol)
88
- @parsed[symbol]
89
- end
90
-
91
- #Digests an XML DOM node into nested Ruby types.
92
- def SoapResponse.parse(node)
93
- #Convert text nodes into simple strings.
94
- return node.text unless node.has_elements?
95
-
96
- #Convert nodes with children into FlashHashes.
97
- elements = {}
98
- class << elements
99
- include FlashHash
100
- end
101
-
102
- #Add all the element's children to the hash.
103
- node.each_element do |e|
104
- name = e.name.to_sym
105
-
106
- case elements[name]
107
- #The most common case: unique child element tags.
108
- when NilClass: elements[name] = parse(e)
109
-
110
- #Non-unique child elements become arrays:
63
+ require 'rforce/binding'
64
+ require 'rforce/soap_response_rexml'
65
+ begin; require 'rforce/soap_response_hpricot'; rescue LoadError; end
66
+ begin; require 'rforce/soap_response_expat'; rescue LoadError; end
111
67
 
112
- #We've already created the array: just
113
- #add the element.
114
- when Array: elements[name] << parse(e)
115
68
 
116
- #We haven't created the array yet: do so,
117
- #then put the existing element in, followed
118
- #by the new one.
119
- else
120
- elements[name] = [elements[name]]
121
- elements[name] << parse(e)
122
- end
123
- end
124
-
125
- return elements
126
- end
69
+ module RForce
70
+ # Use the fastest XML parser available.
71
+ def self.parser(name)
72
+ RForce.const_get(name) rescue nil
127
73
  end
128
-
129
- #Expand Ruby data structures into XML.
74
+
75
+ SoapResponse =
76
+ parser(:SoapResponseExpat) ||
77
+ parser(:SoapResponseHpricot) ||
78
+ SoapResponseRexml
79
+
80
+ # Expand Ruby data structures into XML.
130
81
  def expand(builder, args, xmlns = nil)
131
- #Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
82
+ # Nest arrays: [:a, 1, :b, 2] => [[:a, 1], [:b, 2]]
132
83
  if (args.class == Array)
133
84
  args.each_index{|i| args[i, 2] = [args[i, 2]]}
134
85
  end
@@ -136,228 +87,29 @@ module RForce
136
87
  args.each do |key, value|
137
88
  attributes = xmlns ? {:xmlns => xmlns} : {}
138
89
 
139
- #If the XML tag requires attributes,
140
- #the tag name will contain a space
141
- #followed by a string representation
142
- #of a hash of attributes.
90
+ # If the XML tag requires attributes,
91
+ # the tag name will contain a space
92
+ # followed by a string representation
93
+ # of a hash of attributes.
143
94
  #
144
- #e.g. 'sObject {"xsi:type" => "Opportunity"}'
145
- #becomes <sObject xsi:type="Opportunity>...</sObject>
95
+ # e.g. 'sObject {"xsi:type" => "Opportunity"}'
96
+ # becomes <sObject xsi:type="Opportunity>...</sObject>
146
97
  if key.is_a? String
147
98
  key, modifier = key.split(' ', 2)
148
99
 
149
100
  attributes.merge!(eval(modifier)) if modifier
150
101
  end
151
102
 
152
- #Create an XML element and fill it with this
153
- #value's sub-items.
103
+ # Create an XML element and fill it with this
104
+ # value's sub-items.
154
105
  case value
155
- when Hash, Array
106
+ when Hash, Array then
156
107
  builder.tag!(key, attributes) do expand builder, value; end
157
108
 
158
- when String
109
+ when String then
159
110
  builder.tag!(key, attributes) { builder.text! value }
160
111
  end
161
112
  end
162
113
  end
163
114
 
164
- #Implements the connection to the SalesForce server.
165
- class Binding
166
- include RForce
167
-
168
- DEFAULT_BATCH_SIZE = 10
169
- attr_accessor :batch_size, :url, :assignment_rule_id, :use_default_rule, :update_mru, :client_id, :trigger_user_email,
170
- :trigger_other_email, :trigger_auto_response_email
171
-
172
- #Fill in the guts of this typical SOAP envelope
173
- #with the session ID and the body of the SOAP request.
174
- Envelope = <<-HERE
175
- <?xml version="1.0" encoding="utf-8" ?>
176
- <soap:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"
177
- xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
178
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
179
- xmlns:partner="urn:partner.soap.sforce.com">
180
- xmlns:spartner="urn:sobject.partner.soap.sforce.com">
181
- <soap:Header>
182
- <partner:SessionHeader soap:mustUnderstand='1'>
183
- <partner:sessionId>%s</partner:sessionId>
184
- </partner:SessionHeader>
185
- <partner:QueryOptions soap:mustUnderstand='1'>
186
- <partner:batchSize>%d</partner:batchSize>
187
- </partner:QueryOptions>
188
- %s
189
- </soap:Header>
190
- <soap:Body>
191
- %s
192
- </soap:Body>
193
- </soap:Envelope>
194
- HERE
195
-
196
- AssignmentRuleHeaderUsingRuleId = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:assignmentRuleId>%s</partner:assignmentRuleId></partner:AssignmentRuleHeader>'
197
- AssignmentRuleHeaderUsingDefaultRule = '<partner:AssignmentRuleHeader soap:mustUnderstand="1"><partner:useDefaultRule>true</partner:useDefaultRule></partner:AssignmentRuleHeader>'
198
- MruHeader = '<partner:MruHeader soap:mustUnderstand="1"><partner:updateMru>true</partner:updateMru></partner:MruHeader>'
199
- ClientIdHeader = '<partner:CallOptions soap:mustUnderstand="1"><partner:client>%s</partner:client></partner:CallOptions>'
200
-
201
- #Connect to the server securely.
202
- def initialize(url, sid = nil)
203
- init_server(url)
204
-
205
- @session_id = sid
206
- @batch_size = DEFAULT_BATCH_SIZE
207
- end
208
-
209
-
210
- def show_debug
211
- ENV['SHOWSOAP'] == 'true'
212
- end
213
-
214
-
215
- def init_server(url)
216
- @url = URI.parse(url)
217
- @server = Net::HTTP.new(@url.host, @url.port)
218
- @server.use_ssl = @url.scheme == 'https'
219
- @server.verify_mode = OpenSSL::SSL::VERIFY_NONE
220
-
221
- # run ruby with -d or env variable SHOWSOAP=true to see SOAP wiredumps.
222
- @server.set_debug_output $stderr if show_debug
223
- end
224
-
225
-
226
- #Log in to the server and remember the session ID
227
- #returned to us by SalesForce.
228
- def login(user, password)
229
- @user = user
230
- @password = password
231
-
232
- response = call_remote(:login, [:username, user, :password, password])
233
-
234
- raise "Incorrect user name / password [#{response.fault}]" unless response.loginResponse
235
-
236
- result = response[:loginResponse][:result]
237
- @session_id = result[:sessionId]
238
-
239
- init_server(result[:serverUrl])
240
-
241
- response
242
- end
243
-
244
-
245
- #Call a method on the remote server. Arguments can be
246
- #a hash or (if order is important) an array of alternating
247
- #keys and values.
248
- def call_remote(method, args)
249
- #Create XML text from the arguments.
250
- expanded = ''
251
- @builder = Builder::XmlMarkup.new(:target => expanded)
252
- expand(@builder, {method => args}, 'urn:partner.soap.sforce.com')
253
-
254
- extra_headers = ""
255
- extra_headers << (AssignmentRuleHeaderUsingRuleId % assignment_rule_id) if assignment_rule_id
256
- extra_headers << AssignmentRuleHeaderUsingDefaultRule if use_default_rule
257
- extra_headers << MruHeader if update_mru
258
- extra_headers << (ClientIdHeader % client_id) if client_id
259
-
260
- if trigger_user_email or trigger_other_email or trigger_auto_response_email
261
- extra_headers << '<partner:EmailHeader soap:mustUnderstand="1">'
262
-
263
- extra_headers << '<partner:triggerUserEmail>true</partner:triggerUserEmail>' if trigger_user_email
264
- extra_headers << '<partner:triggerOtherEmail>true</partner:triggerOtherEmail>' if trigger_other_email
265
- extra_headers << '<partner:triggerAutoResponseEmail>true</partner:triggerAutoResponseEmail>' if trigger_auto_response_email
266
-
267
- extra_headers << '</partner:EmailHeader>'
268
- end
269
-
270
- #Fill in the blanks of the SOAP envelope with our
271
- #session ID and the expanded XML of our request.
272
- request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
273
-
274
- # reset the batch size for the next request
275
- @batch_size = DEFAULT_BATCH_SIZE
276
-
277
- # gzip request
278
- request = encode(request)
279
-
280
- headers = {
281
- 'Connection' => 'Keep-Alive',
282
- 'Content-Type' => 'text/xml',
283
- 'SOAPAction' => '""',
284
- 'User-Agent' => 'activesalesforce rforce/1.0'
285
- }
286
-
287
- unless show_debug
288
- headers['Accept-Encoding'] = 'gzip'
289
- headers['Content-Encoding'] = 'gzip'
290
- end
291
-
292
- #Send the request to the server and read the response.
293
- response = @server.post2(@url.path, request.lstrip, headers)
294
-
295
- # decode if we have encoding
296
- content = decode(response)
297
-
298
- # Check to see if INVALID_SESSION_ID was raised and try to relogin in
299
- if method != :login and @session_id and content =~ /sf:INVALID_SESSION_ID/
300
- login(@user, @password)
301
-
302
- # repackage and rencode request with the new session id
303
- request = (Envelope % [@session_id, @batch_size, extra_headers, expanded])
304
- request = encode(request)
305
-
306
- #Send the request to the server and read the response.
307
- response = @server.post2(@url.path, request.lstrip, headers)
308
-
309
- content = decode(response)
310
- end
311
-
312
- SoapResponse.new(content)
313
- end
314
-
315
-
316
- # decode gzip
317
- def decode(response)
318
- encoding = response['Content-Encoding']
319
-
320
- # return body if no encoding
321
- if !encoding then return response.body end
322
-
323
- # decode gzip
324
- case encoding.strip
325
- when 'gzip':
326
- begin
327
- gzr = Zlib::GzipReader.new(StringIO.new(response.body))
328
- decoded = gzr.read
329
- ensure
330
- gzr.close
331
- end
332
- decoded
333
- else
334
- response.body
335
- end
336
- end
337
-
338
-
339
- # encode gzip
340
- def encode(request)
341
- return request if show_debug
342
-
343
- begin
344
- ostream = StringIO.new
345
- gzw = Zlib::GzipWriter.new(ostream)
346
- gzw.write(request)
347
- ostream.string
348
- ensure
349
- gzw.close
350
- end
351
- end
352
-
353
-
354
- #Turns method calls on this object into remote SOAP calls.
355
- def method_missing(method, *args)
356
- unless args.size == 1 && [Hash, Array].include?(args[0].class)
357
- raise 'Expected 1 Hash or Array argument'
358
- end
359
-
360
- call_remote method, args[0]
361
- end
362
- end
363
115
  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}]" 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' then
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
+