├── .gitignore ├── Gemfile ├── Rakefile ├── spec ├── spec_helper.rb └── multiparty_spec.rb ├── Gemfile.lock ├── multiparty.gemspec ├── LICENSE ├── lib └── multiparty.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .rspec 2 | *.gem 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | group :test do 4 | gem 'rspec' 5 | gem 'rake' 6 | gem 'mime-types' 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :spec 2 | task :test => :spec 3 | 4 | desc "Run specs" 5 | task :spec do 6 | exec "rspec spec/multiparty_spec.rb" 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler.setup(:default, :test) 4 | Bundler.require(:default, :test) 5 | 6 | require 'rspec' 7 | 8 | $:.unshift File.join(File.dirname(__FILE__), '..', 'lib') 9 | require "multiparty" 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | diff-lcs (1.1.3) 5 | mime-types (1.16) 6 | rake (0.9.2) 7 | rspec (2.6.0) 8 | rspec-core (~> 2.6.0) 9 | rspec-expectations (~> 2.6.0) 10 | rspec-mocks (~> 2.6.0) 11 | rspec-core (2.6.4) 12 | rspec-expectations (2.6.0) 13 | diff-lcs (~> 1.1.2) 14 | rspec-mocks (2.6.0) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | mime-types 21 | rake 22 | rspec 23 | -------------------------------------------------------------------------------- /multiparty.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'multiparty' 3 | s.version = '0.2.0' 4 | s.summary = 'Easily generate a multipart/form-data header and body.' 5 | s.authors = ['David Verhasselt'] 6 | s.email = 'david@crowdway.com' 7 | s.homepage = 'http://github.com/dv/multiparty' 8 | 9 | files = %w(README.md Rakefile LICENSE) 10 | files += Dir.glob("lib/**/*") 11 | files += Dir.glob("spec/**/*") 12 | s.files = files 13 | 14 | s.add_dependency 'mime-types' 15 | 16 | s.description = < "my-boundary") 10 | def initialize(options = {}) 11 | case options 12 | when Hash 13 | @boundary = options[:boundary] 14 | @content_type = options[:content_type] 15 | when String 16 | @boundary = options 17 | end 18 | 19 | @parts = {} 20 | @content_type ||= "multipart/form-data" 21 | @boundary ||= "multiparty-boundary-#{rand(1000000000)}" 22 | 23 | yield self if block_given? 24 | end 25 | 26 | def header 27 | "Content-Type: #{header_value}\r\n" 28 | end 29 | 30 | def header_value 31 | "#{@content_type}; boundary=#{@boundary}" 32 | end 33 | 34 | def body 35 | result = "--#{@boundary}\r\n" 36 | result << parts.map do |name, value| 37 | parse_part(name, value) 38 | end.join("\r\n") 39 | 40 | result << "--" 41 | end 42 | alias_method :to_s, :body 43 | 44 | def parse_part(name, value) 45 | content_disposition = "form-data" 46 | case value 47 | when Hash 48 | content_disposition = value[:content_disposition] if value[:content_disposition] 49 | content_type = value[:content_type] 50 | filename = value[:filename] 51 | encoding = value[:encoding] 52 | body_part = value[:content] 53 | when File, Tempfile 54 | content_type = "application/octet-stream" 55 | filename = File.split(value.path).last 56 | body_part = value.read 57 | when Array 58 | return value.map { |v| parse_part("#{name}[]", v) }.join "\r\n" 59 | else 60 | body_part = value 61 | end 62 | 63 | if filename 64 | content_type ||= MIME::Types.of(filename).first.to_s || "application/octet-stream" 65 | encoding ||= "binary" 66 | end 67 | 68 | head_part = "Content-Disposition: #{content_disposition}; name=\"#{name}\"" 69 | head_part << "; filename=\"#{filename}\"" if filename 70 | head_part << "\r\n" 71 | head_part << "Content-Type: #{content_type}\r\n" if content_type 72 | head_part << "Content-Transfer-Encoding: #{encoding}\r\n" if encoding 73 | 74 | "#{head_part}\r\n#{body_part}\r\n--#{@boundary}" 75 | end 76 | 77 | def get_part(index) 78 | parts[index] 79 | end 80 | alias_method :[], :get_part 81 | 82 | def add_part(index, value) 83 | parts[index] = value 84 | end 85 | alias_method :[]=, :add_part 86 | 87 | def add_parts(parts) 88 | parts.each do |index, value| 89 | add_part(index, value) 90 | end 91 | end 92 | alias_method :<<, :add_parts 93 | 94 | end 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | multiparty 2 | ========== 3 | 4 | Easily generate a multipart/form-data header and body. 5 | 6 | Usage 7 | ----- 8 | 9 | You can add multiple values, corresponding to multiple statements: 10 | 11 | ```ruby 12 | multiparty = Multiparty.new 13 | multiparty[:name] = "David Verhasselt" 14 | multiparty[:state] = "awesome" 15 | # or in one statement: 16 | multiparty << {:name => "David Verhasselt", :state => "awesome"} 17 | 18 | multiparty[:avatar] = {:filename => "avatar.jpg", :content => "...jpegdata..."} 19 | 20 | # Retrieve the header and body like this: 21 | multiparty.header 22 | # Content-Type: multipart/form-data; boundary=multiparty-boundary-1342 23 | multiparty.body 24 | # --multiparty-boundary-1342 25 | # Content-Disposition: form-data; name="name" 26 | # 27 | # David Verhasselt 28 | # --multiparty-boundary-1342 29 | # Content-Disposition: form-data; name="state" 30 | # 31 | # awesome 32 | # --multiparty-boundary-1342 33 | # Content-Disposition: form-data; name="avatar"; filename="avatar.jpg" 34 | # Content-Type: application/octet-stream 35 | # Content-Transfer-Encoding: binary 36 | # 37 | # ...jpegdata... 38 | # --multiparty-boundary-1342-- 39 | ``` 40 | 41 | You can also add files: 42 | 43 | ```ruby 44 | multiparty[:your_avatar] = File.open("foo.txt") 45 | ``` 46 | 47 | You can specify an optional content-type. If you don't, Multiparty will try and detect the correct MIME-type based on the filename. 48 | 49 | ```ruby 50 | multiparty[:your_avatar] = {:filename => "foo.jpg", :content_type => "text/plain", :content => File.read("foo.txt")} 51 | # -> Content-Type: text/plain 52 | multiparty[:your_avatar] = {:filename => "foo.jpg", :content => "not really jpeg")} 53 | # -> Content-Type: image/jpeg 54 | multiparty[:your_avatar] = File.open("foo.jpg") 55 | # -> Content-Type: image/jpeg 56 | ``` 57 | 58 | Files and Tempfiles are interchangable in Multiparty: 59 | 60 | ```ruby 61 | tempfile = Tempfile.new("foo") 62 | tempfile.write("Hello World!") 63 | tempfile.rewind 64 | 65 | multiparty[:message] = tempfile 66 | # is the same as 67 | multiparty[:message] = File.open(tempfile.path) 68 | ``` 69 | 70 | Arrays are handled in the conventional way using "array[]" as name: 71 | 72 | ```ruby 73 | multiparty[:items] = [1, 2, 3] 74 | 75 | # --AaB03x 76 | # Content-Disposition: form-data; name="items[]" 77 | # 78 | # 1 79 | # --AaB03x 80 | # Content-Disposition: form-data; name="items[]" 81 | # 82 | # 2 83 | # --AaB03x 84 | # Content-Disposition: form-data; name="items[]" 85 | # 86 | # 3 87 | # --AaB03x-- 88 | ``` 89 | 90 | Using the parts accessor you can easily modify parts: 91 | 92 | ```ruby 93 | multiparty[:items] = [1, 2, 3] 94 | multiparty[:items] << 4 95 | 96 | # multiparty[:items] == [1, 2, 3, 4] 97 | ``` 98 | 99 | Multiparty has the ```to_s``` method aliased to ```body``` so you can use it as a ```String```: 100 | 101 | ```ruby 102 | puts "Hello World! My multipart body: #{multiparty}" 103 | ``` 104 | 105 | If the API you're interface with only supports :key => :value headers, use ```header_value```: 106 | 107 | ```ruby 108 | headers["Content-Type"] = multiparty.header_value 109 | ``` 110 | 111 | Installation 112 | ------------ 113 | 114 | $ gem install multiparty 115 | 116 | Testing 117 | ------- 118 | 119 | $ bundle install 120 | $ rake spec 121 | 122 | Todo 123 | ---- 124 | 125 | * Nested multiparts ("multipart/mixed") not yet supported 126 | 127 | Author 128 | ------ 129 | 130 | [David Verhasselt](http://davidverhasselt.com) - david@crowdway.com 131 | -------------------------------------------------------------------------------- /spec/multiparty_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe "multiparty" do 4 | it "should initialize" do 5 | Multiparty.new.should_not be_nil 6 | end 7 | 8 | it "should accept a string as the boundary in #initialize" do 9 | multiparty = Multiparty.new("my-boundary") 10 | multiparty.boundary.should == "my-boundary" 11 | end 12 | 13 | it "should accept an option has in #initialize" do 14 | multiparty = Multiparty.new :boundary => "my-boundary" 15 | multiparty.boundary.should == "my-boundary" 16 | end 17 | 18 | it "should execute a block in #initialize" do 19 | blocktest = false 20 | Multiparty.new do 21 | blocktest = true 22 | end 23 | 24 | blocktest.should be true 25 | end 26 | 27 | describe "instance" do 28 | before(:each) do 29 | @multiparty = Multiparty.new("my-boundary") 30 | end 31 | 32 | it "should return a correct header" do 33 | @multiparty.header.should == "Content-Type: multipart/form-data; boundary=my-boundary\r\n" 34 | @multiparty.header.should == "Content-Type: #{@multiparty.header_value}\r\n" 35 | end 36 | 37 | it "should be able to set-index a key-value pair" do 38 | @multiparty[:key] = :value 39 | @multiparty[:key] = {:filename => "hello.jpg", :content => ""} 40 | end 41 | 42 | it "should be possible to get the key-value pair" do 43 | @multiparty[:key] = :value 44 | @multiparty[:key].should == :value 45 | end 46 | 47 | it "should have a decent #to_s method" do 48 | @multiparty[:key] = :value 49 | @multiparty.body.should == "#{@multiparty}" 50 | end 51 | 52 | it "should be able to add multiple parts at once" do 53 | multiparty1 = Multiparty.new("my-boundary") 54 | multiparty2 = Multiparty.new("my-boundary") 55 | 56 | multiparty1 << {:key1 => :value1, :key2 => :value2} 57 | multiparty2[:key1] = :value1 58 | multiparty2[:key2] = :value2 59 | 60 | multiparty1.body.should == multiparty2.body 61 | end 62 | 63 | it "should return a correctly formed multipart response" do 64 | @multiparty.boundary = "AaB03x" 65 | @multiparty['submit-name'] = "Larry" 66 | @multiparty['files'] = {:filename => "file1.txt", :content_type => "text/plain", :content => "... contents of file1.txt ..."} 67 | 68 | @multiparty.body.gsub("\r\n", "\n").should == '--AaB03x 69 | Content-Disposition: form-data; name="submit-name" 70 | 71 | Larry 72 | --AaB03x 73 | Content-Disposition: form-data; name="files"; filename="file1.txt" 74 | Content-Type: text/plain 75 | Content-Transfer-Encoding: binary 76 | 77 | ... contents of file1.txt ... 78 | --AaB03x--' 79 | end 80 | 81 | it "should accept a File" do 82 | begin 83 | @tempfile = Tempfile.new("foo.txt") 84 | @tempfile.write("Hi world!") 85 | @tempfile.rewind 86 | name = File.split(@tempfile.path).last 87 | 88 | @multiparty[:bar] = @tempfile 89 | @multiparty.body.gsub("\r\n", "\n").should == '--my-boundary 90 | Content-Disposition: form-data; name="bar"; filename="' + name + '" 91 | Content-Type: application/octet-stream 92 | Content-Transfer-Encoding: binary 93 | 94 | Hi world! 95 | --my-boundary--' 96 | ensure 97 | @tempfile.close 98 | @tempfile.unlink 99 | end 100 | end 101 | 102 | it "should accept an array" do 103 | @multiparty[:array] = [1, 2, 3] 104 | @multiparty[:array].should == [1, 2, 3] 105 | end 106 | 107 | it "should be possible to add an element to an added array" do 108 | @multiparty[:array] = [1, 2, 3] 109 | @multiparty[:array] << 4 110 | @multiparty[:array].should == [1, 2, 3, 4] 111 | end 112 | 113 | it "should follow conventions for arrays" do 114 | @multiparty.boundary = "AaB03x" 115 | @multiparty[:array] = [1, 2, 3] 116 | 117 | @multiparty.body.gsub("\r\n", "\n").should == '--AaB03x 118 | Content-Disposition: form-data; name="array[]" 119 | 120 | 1 121 | --AaB03x 122 | Content-Disposition: form-data; name="array[]" 123 | 124 | 2 125 | --AaB03x 126 | Content-Disposition: form-data; name="array[]" 127 | 128 | 3 129 | --AaB03x--' 130 | end 131 | end 132 | end 133 | --------------------------------------------------------------------------------