From c086afee01c51137d511f38a8d9b48605c106ce8 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Tue, 16 Apr 2013 13:54:38 -0700 Subject: [PATCH 1/8] Clean up Gemfile, use https for rubygems It's unlikely that anyone will ever be MITM'd and receive a bad gem, but it's a best practice to use ssl for installing gems --- Gemfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index ee97425..851fabc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,2 @@ -source :rubygems +source 'https://rubygems.org' gemspec - -#gem "savon", :path => "../savon" From 165e86ad48795a0904b4efe79bc1d66428b0a187 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Thu, 11 Apr 2013 22:34:31 -0700 Subject: [PATCH 2/8] Updated README with Savon2 notice --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b7cb9df..1427082 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,20 @@ Savon Multipart [![Build Status](https://secure.travis-ci.org/savonrb/savon-mult =============== Adds multipart support (SOAP with Attachments) to [Savon](https://github.com/savonrb/savon). -Please test and provide feedback so we can merge this into Savon proper. +Please test and provide feedback so we can support as many multipart-soap messages as possible. + + +Savon2 +------ + +Recently Savon launched a rewrite that broke compatibility with savon-multipart. If you're using savon +version 1, this should just work, but if you'd like to use Savon2, you will probably want to specify +something like this in your Gemfile + + +``` +gem 'savon-multipart', :git => 'https://github.com/tjarratt/savon-multipart.git', :branch => 'savon2' +``` Installation From ac02c0479bb3b8f95ddc4fbea96058e283240021 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Sat, 13 Apr 2013 13:34:57 -0700 Subject: [PATCH 3/8] Refactor savon-multipart for Savon2 Needs tests but the interface between savon-multipart and savon just became a whole lot smaller. --- CHANGELOG.md | 5 ++ lib/savon-multipart.rb | 8 ++-- lib/savon/multipart/response.rb | 54 ++++++++++++++++++++++ lib/savon/multipart/soap/part.rb | 14 ------ lib/savon/multipart/soap/request.rb | 29 ------------ lib/savon/multipart/soap/response.rb | 68 ---------------------------- lib/savon/multipart/soap/xml.rb | 50 -------------------- lib/savon/multipart/version.rb | 2 +- savon-multipart.gemspec | 2 +- 9 files changed, 64 insertions(+), 168 deletions(-) create mode 100644 lib/savon/multipart/response.rb delete mode 100644 lib/savon/multipart/soap/part.rb delete mode 100644 lib/savon/multipart/soap/request.rb delete mode 100644 lib/savon/multipart/soap/response.rb delete mode 100644 lib/savon/multipart/soap/xml.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb58e9..530ebc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,8 @@ * Initial version. Adds multipart support (SOAP with Attachments) to Savon v0.9.7. Please test and provide feedback so we can merge this into Savon proper. + +## 2.0.0 (UNRELEASED) +* Rewrite of savon-multipart to work with Savon 2.0+ + This includes all of the existing functionality in savon-multipart + for savon1, but in a way that will be easier to maintain. diff --git a/lib/savon-multipart.rb b/lib/savon-multipart.rb index 3399207..e68ec01 100644 --- a/lib/savon-multipart.rb +++ b/lib/savon-multipart.rb @@ -1,5 +1,3 @@ -require "savon" -require "savon/multipart/version" -require "savon/multipart/soap/request" -require "savon/multipart/soap/response" -require "savon/multipart/soap/xml" +require 'savon' +require 'savon/multipart/version' +require 'savon/multipart/response' diff --git a/lib/savon/multipart/response.rb b/lib/savon/multipart/response.rb new file mode 100644 index 0000000..1eca031 --- /dev/null +++ b/lib/savon/multipart/response.rb @@ -0,0 +1,54 @@ +require 'mail' + +module Savon + module Multipart + class Response < Savon::Response + attr_reader :parts + + def initialize(*args) + @parts = [] + super + end + + def attachments + if multipart? + parse_body unless @has_parsed_body + @parts.attachments + else + [] + end + end + + def to_xml + if multipart? + parse_body unless @has_parsed_body + @parts.first.body + else + super + end + end + + private + def multipart? + http.headers['Content-Type'] =~ /^multipart/ + end + + def boundary + return unless multipart? + @boundary ||= Mail::Field.new('Content-Type', http.headers['Content-Type']).parameters['boundary'] + end + + def parse_body + unless multipart? + super + else + @parts = Mail::Part.new( + :headers => http.headers, + :body => http.body + ).body.split!(boundary).parts + @has_parsed_body = true + end + end + end + end +end diff --git a/lib/savon/multipart/soap/part.rb b/lib/savon/multipart/soap/part.rb deleted file mode 100644 index 12c052d..0000000 --- a/lib/savon/multipart/soap/part.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "mail" - -module Savon - module SOAP - - # = Savon::SOAP::Part - # - # Represents a part in a multipart SOAP request. - class Part < Mail::Part - # placeholder for SOAP-sepcific improvements... some day. - end - - end -end diff --git a/lib/savon/multipart/soap/request.rb b/lib/savon/multipart/soap/request.rb deleted file mode 100644 index bea4a8c..0000000 --- a/lib/savon/multipart/soap/request.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Savon - module SOAP - class Request - - private - - def configure(http) - http.url = soap.endpoint - if soap.has_parts? # do multipart stuff if soap has parts - request_message = soap.request_message - # takes relevant http headers from the "Mail" message and makes - # them Net::HTTP compatible - request_message.header.fields.each do |field| - http.headers[field.name] = field.to_s - end - #request.headers["Content-Type"] << %|; start=""| - request_message.body.set_sort_order soap.parts_sort_order if soap.parts_sort_order && soap.parts_sort_order.any? - http.body = request_message.body.encoded - else - http.body = soap.to_xml - end - http.headers["Content-Type"] ||= CONTENT_TYPE[soap.version] - http.headers["Content-Length"] = http.body.bytesize.to_s - http - end - - end - end -end diff --git a/lib/savon/multipart/soap/response.rb b/lib/savon/multipart/soap/response.rb deleted file mode 100644 index 0262ea5..0000000 --- a/lib/savon/multipart/soap/response.rb +++ /dev/null @@ -1,68 +0,0 @@ -require "savon/multipart/soap/part" - -module Savon - module SOAP - class Response - - # Overwrite to +decode_multipart+. - def initialize(config, response) - self.http = response - decode_multipart - raise_errors if config.raise_errors - end - - def parts - @parts || [] - end - - attr_writer :parts - - # Returns +true+ if this is a multipart response. - def multipart? - http.headers["Content-Type"] =~ /^multipart/ - end - - # Returns the boundary declaration of the multipart response. - def boundary - return unless multipart? - @boundary ||= Mail::Field.new("Content-Type", http.headers["Content-Type"]).parameters['boundary'] - end - - # Returns the Array of attachments if it was a multipart response. - def attachments - parts.attachments - end - - # Overwrite to work with multipart response. - def to_xml - if multipart? - parts.first.body.decoded # we just assume the first part is the XML - else - http.body - end - end - - private - - # Decoding multipart responses. - # - # response.to_xml will point to the first part, hopefully the SOAP part of the multipart. - # All attachments are available in the response.parts Array. Each is a Part from the mail gem. - # See the docs there for details but: - # - # * response.parts[0].body is the contents - # * response.parts[0].headers are the mime headers - # - # And you can do nesting: - # - # * response.parts[0].parts[2].body - def decode_multipart - return unless multipart? - part_of_parts = Part.new(:headers => http.headers, :body => http.body) - part_of_parts.body.split!(boundary) - self.parts = part_of_parts.parts - end - - end - end -end diff --git a/lib/savon/multipart/soap/xml.rb b/lib/savon/multipart/soap/xml.rb deleted file mode 100644 index a7f56d0..0000000 --- a/lib/savon/multipart/soap/xml.rb +++ /dev/null @@ -1,50 +0,0 @@ -require "savon/multipart/soap/part" - -module Savon - module SOAP - class XML - - # Use sort functionality in Mail::Body.sort!() to order parts. - # An array of mime types is expected. - # E.g. this makes the xml appear before an attached image: ["text/xml", "image/jpeg"] - attr_accessor :parts_sort_order - - # Adds a Part object to the current SOAP "message". - # Parts are really attachments. - def add_part(part) - @parts ||= Array.new - @parts << part - end - - # Check if any parts have been added. - def has_parts? - @parts ||= Array.new - !@parts.empty? - end - - # Returns the mime message for a multipart request. - def request_message - return if @parts.empty? - - @request_message = Part.new do - content_type 'multipart/related; type="text/xml"' - end - - soap_body = self.to_xml - soap_message = Part.new do - content_type 'text/xml; charset=utf-8' - add_content_transfer_encoding - body soap_body - end - soap_message.add_content_id "" - @request_message.add_part(soap_message) - @parts.each do |part| - @request_message.add_part(part) - end - #puts @request_message - @request_message - end - - end - end -end diff --git a/lib/savon/multipart/version.rb b/lib/savon/multipart/version.rb index 6470517..292a1bc 100644 --- a/lib/savon/multipart/version.rb +++ b/lib/savon/multipart/version.rb @@ -1,7 +1,7 @@ module Savon module Multipart - VERSION = "1.2.0" + VERSION = "2.0.0" end end diff --git a/savon-multipart.gemspec b/savon-multipart.gemspec index d7ef995..e925a73 100644 --- a/savon-multipart.gemspec +++ b/savon-multipart.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.rubyforge_project = s.name - s.add_dependency "savon", "1.2.0" + s.add_dependency "savon", "~> 2.1.0" s.add_dependency "mail" s.add_development_dependency "rake", "~> 0.8.7" From d3f1c87b1ffa9c92e1079aa1e730f2e7e76562a4 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Tue, 16 Apr 2013 14:03:14 -0700 Subject: [PATCH 4/8] Remove spec_helper This isn't necessary anymore because Savon does not have a global configuration manager in version 2. Instead, when you instantiate a client, you have an option to pass in some options there, and you can also pass in options (that will take precedence over the previous ones) when you call the #call method on a Savon::Client Neat, and with way less global state than before. Either way, this spec helper isn't needed anymore. --- spec/savon/soap/response_spec.rb | 2 +- spec/spec_helper.rb | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 spec/spec_helper.rb diff --git a/spec/savon/soap/response_spec.rb b/spec/savon/soap/response_spec.rb index 27e53f6..6722585 100644 --- a/spec/savon/soap/response_spec.rb +++ b/spec/savon/soap/response_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require 'savon-multipart' describe Savon::SOAP::Response do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index a2a8b52..0000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "bundler" -Bundler.require :default, :development - -require "savon-multipart" - -# Disable logging and deprecations for specs. -Savon.configure do |config| - config.log = false - # config.deprecate = false -end From 41b1c1bbc2b6088c12a282b6d694b53f0d969041 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Tue, 16 Apr 2013 16:52:03 -0700 Subject: [PATCH 5/8] Update rspec tests Nothing crazy, just changed how we create the response --- spec/fixtures/response/not_multipart.txt | 1 + spec/savon/soap/response_spec.rb | 31 +++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/response/not_multipart.txt diff --git a/spec/fixtures/response/not_multipart.txt b/spec/fixtures/response/not_multipart.txt new file mode 100644 index 0000000..ca19c91 --- /dev/null +++ b/spec/fixtures/response/not_multipart.txt @@ -0,0 +1 @@ +20110127135358111111111115.3.0messagingADM1111111111111111Personal2011-01-28T13:53:58ZfalsefalseNormalTest MMS via SavonSender diff --git a/spec/savon/soap/response_spec.rb b/spec/savon/soap/response_spec.rb index 6722585..bc35f3e 100644 --- a/spec/savon/soap/response_spec.rb +++ b/spec/savon/soap/response_spec.rb @@ -1,6 +1,6 @@ require 'savon-multipart' -describe Savon::SOAP::Response do +describe Savon::Multipart::Response do before do @header = { "Content-Type" => 'multipart/related; boundary="--==_mimepart_4d416ae62fd32_201a8043814c4724"; charset=UTF-8; type="text/xml"' } @@ -22,11 +22,36 @@ response.attachments.size.should == 1 end + it "parses soap messages without attachments too" do + header = { 'Content-Type' => 'text/html; charset=utf-8'} + body = File.read(File.expand_path('../../../fixtures/response/not_multipart.txt', __FILE__)) + response = soap_response :headers => header, :body => body + + response.to_xml.chomp.should == '20110127135358111111111115.3.0messagingADM1111111111111111Personal2011-01-28T13:53:58ZfalsefalseNormalTest MMS via SavonSender' + response.parts.size.should == 0 + response.attachments.size.should == 0 + end + + it "only parses the SOAP body once" do + response = soap_response :headers => @header, :body => @body + response.to_xml + + counter = 0 + subbed_parse_body = lambda { counter += 1 } + response.send(:define_singleton_method, :parse_body, subbed_parse_body) + 5.times { response.attachments } + + counter.should == 0 + end + def soap_response(options = {}) defaults = { :code => 200, :headers => {}, :body => "" } response = defaults.merge options + globals = {:multipart => true} + locals = {} + http = HTTPI::Response.new(response[:code], response[:headers], response[:body]) - Savon::SOAP::Response.new Savon.config, HTTPI::Response.new(response[:code], response[:headers], response[:body]) - end + Savon::Multipart::Response.new(http, globals, locals) +end end From 76afc55d6ada093f08fb130e8b9cf6f9bfc35b3c Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Wed, 1 May 2013 15:32:36 -0700 Subject: [PATCH 6/8] Cleanup some whitespace --- spec/savon/soap/response_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/savon/soap/response_spec.rb b/spec/savon/soap/response_spec.rb index bc35f3e..041d5bb 100644 --- a/spec/savon/soap/response_spec.rb +++ b/spec/savon/soap/response_spec.rb @@ -52,6 +52,5 @@ def soap_response(options = {}) http = HTTPI::Response.new(response[:code], response[:headers], response[:body]) Savon::Multipart::Response.new(http, globals, locals) -end - + end end From cb90ee4efd7582d4dc6ba80cab41a37f24c09399 Mon Sep 17 00:00:00 2001 From: Tim Jarratt Date: Wed, 1 May 2013 15:40:35 -0700 Subject: [PATCH 7/8] Add tests for the #body and #to_xml methods I had previously only been using the #to_xml and #parts methods, so I hadn't run this this before. The issue was that Nori does not know how to handle an instance of Mail::Body rather than a string. It seems that the correct thing to do is to just return a string. Also brought in some same defaults from Savon::GlobalOptions for this test. Before it wouldn't have been possible to actually call the #body method in the tests because we didn't specify how to take the response body tags and turn them into snakecase symbols --- lib/savon/multipart/response.rb | 16 +++++-------- spec/fixtures/response/simple_multipart.txt | 25 +++++++++++++++++++++ spec/savon/soap/response_spec.rb | 21 ++++++++++++++--- 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 spec/fixtures/response/simple_multipart.txt diff --git a/lib/savon/multipart/response.rb b/lib/savon/multipart/response.rb index 1eca031..d81b572 100644 --- a/lib/savon/multipart/response.rb +++ b/lib/savon/multipart/response.rb @@ -22,7 +22,7 @@ def attachments def to_xml if multipart? parse_body unless @has_parsed_body - @parts.first.body + @parts.first.body.to_s else super end @@ -39,15 +39,11 @@ def boundary end def parse_body - unless multipart? - super - else - @parts = Mail::Part.new( - :headers => http.headers, - :body => http.body - ).body.split!(boundary).parts - @has_parsed_body = true - end + @parts = Mail::Part.new( + :headers => http.headers, + :body => http.body + ).body.split!(boundary).parts + @has_parsed_body = true end end end diff --git a/spec/fixtures/response/simple_multipart.txt b/spec/fixtures/response/simple_multipart.txt new file mode 100644 index 0000000..6b97b2b --- /dev/null +++ b/spec/fixtures/response/simple_multipart.txt @@ -0,0 +1,25 @@ +----==_mimepart_4d416ae62fd32_201a8043814c4724 +Date: Thu, 27 Jan 2011 13:53:58 +0100 +Message-ID: <4d416ae631391_201a8043814c482c@Martin-iMac.local.mail> +Mime-Version: 1.0 +Content-Type: text/xml; + charset=utf-8 +Content-Transfer-Encoding: 7bit +content-id: soap_xml_part + +
true
+ +----==_mimepart_4d416ae628621_201a8043814c46ea +Date: Thu, 27 Jan 2011 13:53:58 +0100 +Message-ID: <4d416ae633ce8_201a8043814c49c7@Martin-iMac.local.mail> +Mime-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +content-id: text_0.txt + +This is a test message from Github + +----==_mimepart_4d416ae628621_201a8043814c46ea-- + +----==_mimepart_4d416ae62fd32_201a8043814c4724-- diff --git a/spec/savon/soap/response_spec.rb b/spec/savon/soap/response_spec.rb index 041d5bb..f99f2af 100644 --- a/spec/savon/soap/response_spec.rb +++ b/spec/savon/soap/response_spec.rb @@ -17,6 +17,19 @@ response.parts[1].parts[2].body.should == "This is a test message from Github" end + it "returns a String from the #to_xml method" do + body = File.read(File.expand_path('../../../fixtures/response/simple_multipart.txt', __FILE__)) + response = soap_response :headers => @header, :body => body + response.to_xml.class.should == String + end + + it "returns a Hash from the #body method" do + body = File.read(File.expand_path('../../../fixtures/response/simple_multipart.txt', __FILE__)) + response = soap_response :headers => @header, :body => body + response.body.class.should == Hash + response.body.should == {:submit_req => true} + end + it "returns the attachments" do response = soap_response :headers => @header, :body => @body response.attachments.size.should == 1 @@ -47,10 +60,12 @@ def soap_response(options = {}) defaults = { :code => 200, :headers => {}, :body => "" } response = defaults.merge options - globals = {:multipart => true} - locals = {} + globals = { + :multipart => true, + :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym} + } http = HTTPI::Response.new(response[:code], response[:headers], response[:body]) - Savon::Multipart::Response.new(http, globals, locals) + Savon::Multipart::Response.new(http, globals, {}) end end From 8f6f947e695e55eca23b78a29e6c7ffed1b8bf02 Mon Sep 17 00:00:00 2001 From: "tjarratt@gmail.com" Date: Mon, 6 May 2013 22:55:25 -0700 Subject: [PATCH 8/8] Update tests to pass on 1.8.7 Unsure whether this will pass on ree, but it will pass on all supported versions of 1.8.7 (p371 mri, jruby18-mode and rubinius). --- spec/savon/soap/response_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/savon/soap/response_spec.rb b/spec/savon/soap/response_spec.rb index f99f2af..cfa3922 100644 --- a/spec/savon/soap/response_spec.rb +++ b/spec/savon/soap/response_spec.rb @@ -51,7 +51,7 @@ counter = 0 subbed_parse_body = lambda { counter += 1 } - response.send(:define_singleton_method, :parse_body, subbed_parse_body) + response.class.send(:define_method, :parse_body, subbed_parse_body) 5.times { response.attachments } counter.should == 0