troelskn-handsoap 0.4.4 → 0.5.0

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.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :minor: 4
3
- :patch: 4
2
+ :minor: 5
3
+ :patch: 0
4
4
  :major: 0
@@ -0,0 +1,407 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Handsoap
3
+
4
+ # The Handsoap::Http module provides a uniform interface to various http drivers.
5
+ module Http
6
+
7
+ # Represents a HTTP Request.
8
+ class Request
9
+ attr_reader :url, :http_method, :headers, :body
10
+ attr_writer :body
11
+ def initialize(url, http_method = :get)
12
+ @url = url
13
+ @http_method = http_method
14
+ @headers = {}
15
+ @body = nil
16
+ end
17
+ def add_header(key, value)
18
+ if @headers[key].nil?
19
+ @headers[key] = []
20
+ end
21
+ @headers[key] << value
22
+ end
23
+ def set_header(key, value)
24
+ if value.nil?
25
+ @headers[key] = nil
26
+ else
27
+ @headers[key] = [value]
28
+ end
29
+ end
30
+ def inspect
31
+ "===============\n" +
32
+ "--- Request ---\n" +
33
+ "URI: #{url}\n" +
34
+ (
35
+ if headers.any?
36
+ "---\n" + headers.map { |key,values| values.map {|value| Handsoap::Http.normalize_header_key(key) + ": " + value + "\n" }.join("") }.join("")
37
+ else
38
+ ""
39
+ end
40
+ ) +
41
+ (
42
+ if body
43
+ "---\n" + body
44
+ else
45
+ ""
46
+ end
47
+ )
48
+ end
49
+ end
50
+
51
+ # Represents a HTTP Part.
52
+ # For simple HTTP-requests there is only one part, which is the response.
53
+ class Part
54
+ attr_reader :headers, :body, :parts
55
+ def initialize(headers, body, parts = nil)
56
+ @headers = headers
57
+ @body = body
58
+ @parts = parts
59
+ end
60
+ # Returns a header.
61
+ # Returns String | Array | nil
62
+ def [](key)
63
+ key.to_s.downcase!
64
+ (@headers[key] && @headers[key].length == 1) ? @headers[key].first : @headers[key]
65
+ end
66
+ # Returns the mime-type part of the content-type header
67
+ def mime_type
68
+ @headers['content-type'].first.match(/^[^;]+/).to_s if @headers['content-type']
69
+ end
70
+ # Returns the charset part of the content-type header
71
+ def charset
72
+ if @headers['content-type']
73
+ match_data = @headers['content-type'].first.match(/^[^;]+; charset=([^;]+)/)
74
+ if match_data
75
+ match_data[1].to_s
76
+ end
77
+ end
78
+ end
79
+ def multipart?
80
+ !! @parts
81
+ end
82
+ def inspect(&block)
83
+ str = inspect_head
84
+ if headers.any?
85
+ str << headers.map { |key,values| values.map {|value| Handsoap::Http.normalize_header_key(key) + ": " + value + "\n" }.join("") }.join("")
86
+ end
87
+ if body
88
+ if multipart?
89
+ if block_given?
90
+ str << parts.map{|part| part.inspect(&block) }.join("")
91
+ else
92
+ str << parts.map{|part| part.inspect }.join("")
93
+ end
94
+ elsif body
95
+ str << "---\n"
96
+ if block_given?
97
+ str << yield(body)
98
+ else
99
+ str << body
100
+ end
101
+ str << "\n---"
102
+ end
103
+ end
104
+ end
105
+ private
106
+ def inspect_head
107
+ "--- Part ---\n"
108
+ end
109
+ end
110
+
111
+ # Represents a HTTP Response.
112
+ class Response < Part
113
+ attr_reader :status
114
+ def initialize(status, headers, body, parts = nil)
115
+ @status = status.to_i
116
+ super(headers, body, parts)
117
+ end
118
+ def primary_part
119
+ # Strictly speaking, the main part doesn't need to be first, but until proven otherwise, we'll just assume that.
120
+ if multipart?
121
+ parts.first
122
+ else
123
+ self
124
+ end
125
+ end
126
+ private
127
+ def inspect_head
128
+ "--- Response ---\n" + "HTTP Status: #{status}\n"
129
+ end
130
+ end
131
+
132
+ # driver for httpclient
133
+ module Httpclient
134
+ def self.load!
135
+ require 'httpclient'
136
+ end
137
+ def self.send_http_request(request)
138
+ self.load!
139
+ http_client = HTTPClient.new
140
+ # pack headers
141
+ headers = request.headers.inject([]) do |arr, (k,v)|
142
+ arr + v.map {|x| [k,x] }
143
+ end
144
+ response = http_client.request(request.http_method, request.url, nil, request.body, headers)
145
+ response_headers = response.header.all.inject({}) do |h, (k, v)|
146
+ k.downcase!
147
+ if h[k].nil?
148
+ h[k] = [v]
149
+ else
150
+ h[k] << v
151
+ end
152
+ h
153
+ end
154
+ Handsoap::Http.parse_http_part(response_headers, response.content, response.status, response.contenttype)
155
+ end
156
+ end
157
+
158
+ # driver for curb
159
+ module Curb
160
+ def self.load!
161
+ require 'curb'
162
+ end
163
+ def self.send_http_request(request)
164
+ self.load!
165
+ http_client = Curl::Easy.new(request.url)
166
+ # pack headers
167
+ headers = request.headers.inject([]) do |arr, (k,v)|
168
+ arr + v.map {|x| "#{k}: #{x}" }
169
+ end
170
+ http_client.headers = headers
171
+ # I don't think put/delete is actually supported ..
172
+ case request.http_method
173
+ when :get
174
+ http_client.http_get
175
+ when :post
176
+ http_client.http_post(request.body)
177
+ when :put
178
+ http_client.http_put(request.body)
179
+ when :delete
180
+ http_client.http_delete
181
+ else
182
+ raise "Unsupported request method #{request.http_method}"
183
+ end
184
+ Handsoap::Http.parse_http_part(http_client.header_str.gsub(/^HTTP.*\r\n/, ""), http_client.body_str, http_client.response_code, http_client.content_type)
185
+ end
186
+ end
187
+
188
+ # driver for net/http
189
+ module NetHttp
190
+ def self.load!
191
+ require 'net/http'
192
+ require 'uri'
193
+ end
194
+ def self.send_http_request(request)
195
+ self.load!
196
+ url = request.url
197
+ unless url.kind_of? ::URI::Generic
198
+ url = ::URI.parse(url)
199
+ end
200
+ ::URI::Generic.send(:public, :path_query) # hackety hack
201
+ path = url.path_query
202
+ http_request = case request.http_method
203
+ when :get
204
+ Net::HTTP::Get.new(path)
205
+ when :post
206
+ Net::HTTP::Post.new(path)
207
+ when :put
208
+ Net::HTTP::Put.new(path)
209
+ when :delete
210
+ Net::HTTP::Delete.new(path)
211
+ else
212
+ raise "Unsupported request method #{request.http_method}"
213
+ end
214
+ http_client = Net::HTTP.new(url.host, url.port)
215
+ http_client.read_timeout = 120
216
+ request.headers.each do |k, values|
217
+ values.each do |v|
218
+ http_request.add_field(k, v)
219
+ end
220
+ end
221
+ http_request.body = request.body
222
+ # require 'stringio'
223
+ # debug_output = StringIO.new
224
+ # http_client.set_debug_output debug_output
225
+ http_response = http_client.start do |client|
226
+ client.request(http_request)
227
+ end
228
+ # puts debug_output.string
229
+ # hacky-wacky
230
+ def http_response.get_headers
231
+ @header.inject({}) do |h, (k, v)|
232
+ h[k.downcase] = v
233
+ h
234
+ end
235
+ end
236
+ Handsoap::Http.parse_http_part(http_response.get_headers, http_response.body, http_response.code)
237
+ end
238
+ end
239
+
240
+ # Parses a raw http response into a +Response+ or +Part+ object.
241
+ def self.parse_http_part(headers, body, status = nil, content_type = nil)
242
+ if headers.kind_of? String
243
+ headers = parse_headers(headers)
244
+ end
245
+ if content_type.nil? && headers['content-type']
246
+ content_type = headers['content-type'].first
247
+ end
248
+ boundary = parse_multipart_boundary(content_type)
249
+ parts = if boundary
250
+ parse_multipart(boundary, body).map {|raw_part| parse_http_part(raw_part[:head], raw_part[:body]) }
251
+ end
252
+ if status.nil?
253
+ Handsoap::Http::Part.new(headers, body, parts)
254
+ else
255
+ Handsoap::Http::Response.new(status, headers, body, parts)
256
+ end
257
+ end
258
+
259
+ # Content-Type header string -> mime-boundary | nil
260
+ def self.parse_multipart_boundary(content_type)
261
+ if %r|\Amultipart.*boundary=\"?([^\";,]+)\"?|n.match(content_type)
262
+ $1.dup
263
+ end
264
+ end
265
+
266
+ # Parses a multipart http-response body into parts.
267
+ # +boundary+ is a string of the boundary token.
268
+ # +content_io+ is either a string or an IO. If it's an IO, then content_length must be specified.
269
+ # +content_length+ (optional) is an integer, specifying the length of +content_io+
270
+ #
271
+ # This code is lifted from cgi.rb
272
+ #
273
+ def self.parse_multipart(boundary, content_io, content_length = nil)
274
+ if content_io.kind_of? String
275
+ content_length = content_io.length
276
+ content_io = StringIO.new(content_io, 'r')
277
+ elsif !(content_io.kind_of? IO) || content_length.nil?
278
+ raise "Second argument must be String or IO with content_length"
279
+ end
280
+
281
+ boundary = "--" + boundary
282
+ quoted_boundary = Regexp.quote(boundary, "n")
283
+ buf = ""
284
+ bufsize = 10 * 1024
285
+ boundary_end = ""
286
+
287
+ # start multipart/form-data
288
+ content_io.binmode if defined? content_io.binmode
289
+ boundary_size = boundary.size + "\r\n".size
290
+ content_length -= boundary_size
291
+ status = content_io.read(boundary_size)
292
+ if nil == status
293
+ raise EOFError, "no content body"
294
+ elsif boundary + "\r\n" != status
295
+ raise EOFError, "bad content body"
296
+ end
297
+
298
+ parts = []
299
+
300
+ loop do
301
+ head = nil
302
+ if 10240 < content_length
303
+ require "tempfile"
304
+ body = Tempfile.new("Handsoap")
305
+ else
306
+ begin
307
+ require "stringio"
308
+ body = StringIO.new
309
+ rescue LoadError
310
+ require "tempfile"
311
+ body = Tempfile.new("Handsoap")
312
+ end
313
+ end
314
+ body.binmode if defined? body.binmode
315
+
316
+ until head and /#{quoted_boundary}(?:\r\n|--)/n.match(buf)
317
+
318
+ if (not head) and /\r\n\r\n/n.match(buf)
319
+ buf = buf.sub(/\A((?:.|\n)*?\r\n)\r\n/n) do
320
+ head = $1.dup
321
+ ""
322
+ end
323
+ next
324
+ end
325
+
326
+ if head and ( ("\r\n" + boundary + "\r\n").size < buf.size )
327
+ body.print buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)]
328
+ buf[0 ... (buf.size - ("\r\n" + boundary + "\r\n").size)] = ""
329
+ end
330
+
331
+ c = if bufsize < content_length
332
+ content_io.read(bufsize)
333
+ else
334
+ content_io.read(content_length)
335
+ end
336
+ if c.nil? || c.empty?
337
+ raise EOFError, "bad content body"
338
+ end
339
+ buf.concat(c)
340
+ content_length -= c.size
341
+ end
342
+
343
+ buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
344
+ body.print $1
345
+ if "--" == $2
346
+ content_length = -1
347
+ end
348
+ boundary_end = $2.dup
349
+ ""
350
+ end
351
+
352
+ body.rewind
353
+ parts << {:head => head, :body => body.read(body.size)}
354
+
355
+ break if buf.size == 0
356
+ break if content_length == -1
357
+ end
358
+ raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/
359
+ parts
360
+ end
361
+
362
+ # lifted from webrick/httputils.rb
363
+ def self.parse_headers(raw)
364
+ header = Hash.new([].freeze)
365
+ field = nil
366
+ raw.each {|line|
367
+ case line
368
+ when /^([A-Za-z0-9!\#$%&'*+\-.^_`|~]+):\s*(.*?)\s*\z/om
369
+ field, value = $1, $2
370
+ field.downcase!
371
+ header[field] = [] unless header.has_key?(field)
372
+ header[field] << value
373
+ when /^\s+(.*?)\s*\z/om
374
+ value = $1
375
+ unless field
376
+ raise "bad header '#{line.inspect}'."
377
+ end
378
+ header[field][-1] << " " << value
379
+ else
380
+ raise "bad header '#{line.inspect}'."
381
+ end
382
+ }
383
+ header.each {|key, values|
384
+ values.each {|value|
385
+ value.strip!
386
+ value.gsub!(/\s+/, " ")
387
+ }
388
+ }
389
+ header
390
+ end
391
+
392
+ def self.normalize_header_key(key)
393
+ key.split("-").map{|s| s.downcase.capitalize }.join("-")
394
+ end
395
+
396
+ @@drivers = {
397
+ :net_http => NetHttp,
398
+ :curb => Curb,
399
+ :httpclient => Httpclient
400
+ }
401
+
402
+ def self.drivers
403
+ @@drivers
404
+ end
405
+
406
+ end
407
+ end
@@ -2,6 +2,7 @@
2
2
  require 'time'
3
3
  require 'handsoap/xml_mason'
4
4
  require 'handsoap/xml_query_front'
5
+ require 'handsoap/http'
5
6
 
6
7
  module Handsoap
7
8
 
@@ -11,8 +12,7 @@ module Handsoap
11
12
 
12
13
  def self.http_driver=(driver)
13
14
  @http_driver = driver
14
- require 'httpclient' if driver == :httpclient
15
- require 'curb' if driver == :curb
15
+ Handsoap::Http.drivers[driver].load!
16
16
  return driver
17
17
  end
18
18
 
@@ -52,6 +52,21 @@ module Handsoap
52
52
  end
53
53
  end
54
54
 
55
+ class SoapResponse
56
+ attr_reader :document, :http_response
57
+ def initialize(document, http_response)
58
+ @document = document
59
+ @http_response = http_response
60
+ end
61
+ def method_missing(method, *args)
62
+ if @document.respond_to?(method)
63
+ @document.__send__ method, *args
64
+ else
65
+ super
66
+ end
67
+ end
68
+ end
69
+
55
70
  class Service
56
71
  @@logger = nil
57
72
  def self.logger=(io)
@@ -152,8 +167,8 @@ module Handsoap
152
167
  # Hook that is called if there is a HTTP level error.
153
168
  #
154
169
  # Default behaviour is to raise an error.
155
- def on_http_error(status, content)
156
- raise "HTTP error #{status}"
170
+ def on_http_error(response)
171
+ raise "HTTP error #{response.status}"
157
172
  end
158
173
  # Hook that is called if the dispatch returns a +Fault+.
159
174
  #
@@ -169,7 +184,7 @@ module Handsoap
169
184
  #
170
185
  # Note that if your service has operations that are one-way, you shouldn't raise an error here.
171
186
  # This is however a fairly exotic case, so that is why the default behaviour is to raise an error.
172
- def on_missing_document(http_response_body)
187
+ def on_missing_document(response)
173
188
  raise "The response is not a valid SOAP envelope"
174
189
  end
175
190
  def debug(message = nil) #:nodoc:
@@ -184,36 +199,21 @@ module Handsoap
184
199
  end
185
200
  # Does the actual HTTP level interaction.
186
201
  def send_http_request(uri, post_body, headers)
187
- if Handsoap.http_driver == :curb
188
- http_client = Curl::Easy.new(uri)
189
- on_after_create_http_client(http_client)
190
- http_client.headers = headers
191
- http_client.http_post post_body
192
- if %r|\Amultipart.*boundary=\"?([^\";,]+)\"?|n.match(http_client.content_type)
193
- boundary = $1.dup
194
- parts = Handsoap.parse_multipart(boundary, http_client.body_str)
195
- is_multipart = true
196
- else
197
- parts = [{:head => http_client.header_str, :body => http_client.body_str}]
198
- is_multipart = false
199
- end
200
- return { :status => http_client.response_code, :body => http_client.body_str, :content_type => http_client.content_type, :parts => parts, :multipart => is_multipart }
201
- elsif Handsoap.http_driver == :httpclient
202
- http_client = HTTPClient.new
203
- on_after_create_http_client(http_client)
204
- response = http_client.post(uri, post_body, headers)
205
- if %r|\Amultipart.*boundary=\"?([^\";,]+)\"?|n.match(response.contenttype)
206
- boundary = $1.dup
207
- parts = Handsoap.parse_multipart(boundary, response.content)
208
- is_multipart = true
209
- else
210
- parts = [{:head => response.header.all.join("\r\n"), :body => response.content}]
211
- is_multipart = false
212
- end
213
- return { :status => response.status, :body => response.content, :content_type => response.contenttype, :parts => parts, :multipart => is_multipart }
214
- else
215
- raise "Unknown http driver #{Handsoap.http_driver}"
202
+ request = Handsoap::Http::Request.new(uri)
203
+ headers.each do |key, value|
204
+ request.add_header(key, value)
216
205
  end
206
+ request.body = post_body
207
+ debug do |logger|
208
+ logger.puts request.inspect
209
+ end
210
+ on_after_create_http_request(request)
211
+ http = Handsoap::Http.drivers[Handsoap.http_driver]
212
+ response = http.send_http_request(request)
213
+ debug do |logger|
214
+ logger.puts response.inspect(&Handsoap.pretty_format_envelope)
215
+ end
216
+ return response
217
217
  end
218
218
  # Send document and parses the response into a +XmlQueryFront::XmlElement+ (XmlDocument)
219
219
  def dispatch(doc, action)
@@ -222,61 +222,26 @@ module Handsoap
222
222
  "Content-Type" => "#{self.class.request_content_type};charset=UTF-8"
223
223
  }
224
224
  headers["SOAPAction"] = action unless action.nil?
225
- body = doc.to_s
226
- debug do |logger|
227
- logger.puts "==============="
228
- logger.puts "--- Request ---"
229
- logger.puts "URI: %s" % [self.class.uri]
230
- logger.puts headers.map { |key,value| key + ": " + value }.join("\n")
231
- logger.puts "---"
232
- logger.puts body
233
- end
234
- response = send_http_request(self.class.uri, body, headers)
235
- debug do |logger|
236
- logger.puts "--- Response ---"
237
- logger.puts "HTTP Status: %s" % [response[:status]]
238
- logger.puts "Content-Type: %s" % [response[:content_type]]
239
- if response[:multipart]
240
- num = 0
241
- response[:parts].each do |part|
242
- num += 1
243
- logger.puts "--- Part ##{num} ---"
244
- logger.puts part[:head].gsub(/\r\n/, "\n")
245
- logger.puts "---"
246
- logger.puts Handsoap.pretty_format_envelope(part[:body])
247
- end
248
- else
249
- logger.puts "---"
250
- logger.puts Handsoap.pretty_format_envelope(response[:body])
251
- end
252
- end
225
+ response = send_http_request(self.class.uri, doc.to_s, headers)
253
226
  # Start the parsing pipe-line.
254
227
  # There are various stages and hooks for each, so that you can override those in your service classes.
255
- xml_document = parse_soap_response_document(response[:parts].first[:body]) # Strictly speaking, the main part doesn't need to be first, but until proven otherwise, we'll just assume that.
228
+ xml_document = parse_soap_response_document(response.primary_part.body)
256
229
  soap_fault = parse_soap_fault(xml_document)
257
230
  # Is the response a soap-fault?
258
231
  unless soap_fault.nil?
259
232
  return on_fault(soap_fault)
260
233
  end
261
234
  # Does the http-status indicate an error?
262
- if response[:status] >= 400
263
- return on_http_error(response[:status], response[:body])
235
+ if response.status >= 400
236
+ return on_http_error(response)
264
237
  end
265
238
  # Does the response contain a valid xml-document?
266
239
  if xml_document.nil?
267
- return on_missing_document(response[:body])
240
+ return on_missing_document(response)
268
241
  end
269
242
  # Everything seems in order.
270
243
  on_response_document(xml_document)
271
- # BC hack
272
- def xml_document.document
273
- self
274
- end
275
- # I should probably use a class for this response object instead ...
276
- def xml_document.parts
277
- response[:parts]
278
- end
279
- return xml_document
244
+ return SoapResponse.new(xml_document, response)
280
245
  end
281
246
  # Creates a standard SOAP envelope and yields the +Body+ element.
282
247
  def make_envelope # :yields: Handsoap::XmlMason::Element
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: troelskn-handsoap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Troels Knak-Nielsen
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-08-11 00:00:00 -07:00
12
+ date: 2009-08-13 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -29,11 +29,12 @@ files:
29
29
  - generators/handsoap/templates/DUMMY
30
30
  - lib/handsoap.rb
31
31
  - lib/handsoap/compiler.rb
32
+ - lib/handsoap/http.rb
32
33
  - lib/handsoap/parser.rb
33
34
  - lib/handsoap/service.rb
34
35
  - lib/handsoap/xml_mason.rb
35
36
  - lib/handsoap/xml_query_front.rb
36
- has_rdoc: false
37
+ has_rdoc: true
37
38
  homepage: http://github.com/troelskn/handsoap
38
39
  licenses:
39
40
  post_install_message:
@@ -62,7 +63,7 @@ requirements:
62
63
  rubyforge_project:
63
64
  rubygems_version: 1.3.5
64
65
  signing_key:
65
- specification_version: 3
66
+ specification_version: 2
66
67
  summary: Handsoap is a library for creating SOAP clients in Ruby
67
68
  test_files: []
68
69