Skip to content

Commit

Permalink
Merge 1c901f2 into fcb56fa
Browse files Browse the repository at this point in the history
  • Loading branch information
dukaarpad committed Jun 23, 2016
2 parents fcb56fa + 1c901f2 commit 2f2773c
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,7 @@
# 2.12.0 (2016-04-29)

* Add multipart request sending option (https://www.w3.org/TR/SOAP-attachments)

# 2.11.1 (2015-05-27)

* Replace dependency on [uuid](https://rubygems.org/gems/uuid), using SecureRandom.uuid instead.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -22,7 +22,7 @@ $ gem install savon
or add it to your Gemfile like this:

```
gem 'savon', '~> 2.11.0'
gem 'savon', '~> 2.12.0'
```

## Usage example
Expand Down
42 changes: 41 additions & 1 deletion lib/savon/builder.rb
Expand Up @@ -6,6 +6,7 @@

module Savon
class Builder
attr_reader :multipart

SCHEMA_TYPES = {
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
Expand Down Expand Up @@ -64,7 +65,46 @@ def build_document
xml = @signature.document
end

xml
# if there are attachments for the request, we should build a multipart message according to
# https://www.w3.org/TR/SOAP-attachments
if @locals[:attachments]
message = Mail.new
xml_part = Mail::Part.new do
content_type 'text/xml'
body xml
# in Content-Type the start parameter is recommended (RFC 2387)
content_id '<soap-request-body@soap>'
end
message.add_part xml_part

if @locals[:attachments].is_a? Hash
@locals[:attachments].each do |content_location, file|
message.add_file file.clone
message.parts.last.content_location = content_location.to_s
message.parts.last.content_id = message.parts.last.content_location
end
elsif @locals[:attachments].is_a? Array
@locals[:attachments].each do |file|
message.add_file file.clone
message.parts.last.content_location = file.is_a?(String) ? File.basename(file) : file[:filename]
message.parts.last.content_id = message.parts.last.content_location
end
end
message.ready_to_send!

# the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
# should redefine the sort order, because the soap request xml should be the first
message.body.set_sort_order [ "text/xml" ]

#request.headers["Content-Type"] = "multipart/related; boundary=\"#{message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
@multipart = {
multipart_boundary: message.body.boundary,
start: xml_part.content_id,
}
message.body.encoded(message.content_transfer_encoding)
else
xml
end
end

def header_attributes
Expand Down
6 changes: 6 additions & 0 deletions lib/savon/operation.rb
Expand Up @@ -5,6 +5,7 @@
require "savon/response"
require "savon/request_logger"
require "savon/http_error"
require "mail"

module Savon
class Operation
Expand Down Expand Up @@ -111,6 +112,11 @@ def build_request(builder)
# was not specified manually? [dh, 2013-01-04]
request.headers["Content-Length"] = request.body.bytesize.to_s

if builder.multipart
request.headers["Content-Type"] = "multipart/related; boundary=\"#{builder.multipart[:multipart_boundary]}\"; " +
"type=\"text/xml\"; start=\"#{builder.multipart[:start]}\""
end

request
end

Expand Down
34 changes: 34 additions & 0 deletions lib/savon/options.rb
Expand Up @@ -383,6 +383,40 @@ def attributes(attributes)
@options[:attributes] = attributes
end

# Attachments for the SOAP message (https://www.w3.org/TR/SOAP-attachments)
#
# should pass an Array or a Hash; items should be path strings or
# { filename: 'file.name', content: 'content' } objects
# The Content-ID in multipart message sections will be the filename or the key if Hash is given
#
# usage examples:
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments [
# { filename: 'x1.xml', content: '<xml>abc</xml>'},
# { filename: 'x2.xml', content: '<xml>abc</xml>'}
# ]
# end
# # Content-ID will be x1.xml and x2.xml
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments 'x1.xml' => '/tmp/1281ab7d7d.xml', 'x2.xml' => '/tmp/4c5v8e833a.xml'
# end
# # Content-ID will be x1.xml and x2.xml
#
# response = client.call :operation1 do
# message param1: 'value'
# attachments [ '/tmp/1281ab7d7d.xml', '/tmp/4c5v8e833a.xml']
# end
# # Content-ID will be 1281ab7d7d.xml and 4c5v8e833a.xml
#
# The Content-ID is important if you want to refer to the attachments from the SOAP request
def attachments(attachments)
@options[:attachments] = attachments
end

# Value of the SOAPAction HTTP header.
def soap_action(soap_action)
@options[:soap_action] = soap_action
Expand Down
2 changes: 1 addition & 1 deletion lib/savon/version.rb
@@ -1,3 +1,3 @@
module Savon
VERSION = '2.11.1'
VERSION = '2.12.0'
end
1 change: 1 addition & 0 deletions savon.gemspec
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |s|
s.add_dependency "gyoku", "~> 1.2"
s.add_dependency "builder", ">= 2.1.2"
s.add_dependency "nokogiri", ">= 1.4.0"
s.add_dependency "mail", "~> 2.5"

s.add_development_dependency "rack"
s.add_development_dependency "puma", "2.0.0.b4"
Expand Down
46 changes: 46 additions & 0 deletions spec/savon/multipart_request_spec.rb
@@ -0,0 +1,46 @@
require "spec_helper"

describe Savon::Builder do

let(:globals) { Savon::GlobalOptions.new({ :endpoint => "http://example.co", :namespace => "http://v1.example.com" }) }
let(:no_wsdl) { Wasabi::Document.new }

it "building multipart request from inline content" do
locals = {
attachments: [
{ filename: 'x1.xml', content: '<xml>abc1</xml>'},
{ filename: 'x2.xml', content: '<xml>abc2</xml>'},
]
}
builder = Savon::Builder.new(:operation1, no_wsdl, globals, Savon::LocalOptions.new(locals))
request_body = builder.to_s

expect(request_body).to include('Content-Type')
expect(request_body).to match(/<[a-z]+:operation1>/)

locals[:attachments].each do |attachment|
expect(request_body).to match(/^Content-Location: #{attachment[:filename]}\s$/)
expect(request_body).to include(Base64.encode64(attachment[:content]).strip)
end

end

it "building multipart request from file" do
locals = {
attachments: {
'file.gz' => File.expand_path("../../fixtures/gzip/message.gz", __FILE__)
}
}
builder = Savon::Builder.new(:operation1, no_wsdl, globals, Savon::LocalOptions.new(locals))
request_body = builder.to_s

expect(request_body).to include('Content-Type')
expect(request_body).to match(/<[a-z]+:operation1>/)

locals[:attachments].each do |id, file|
expect(request_body).to match(/^Content-Location: #{id}\s$/)
expect(request_body.gsub("\r", "")).to include(Base64.encode64(File.read(file)).strip)
end

end
end

0 comments on commit 2f2773c

Please sign in to comment.