philiprehberger-multipart

Tests Gem Version Last updated

Multipart/form-data builder and parser with MIME type detection and streaming support

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-multipart"

Or install directly:

gem install philiprehberger-multipart

Usage

require "philiprehberger/multipart"

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  field :email, 'alice@example.com'
end

builder.to_s          # => multipart body string
builder.content_type  # => "multipart/form-data; boundary=..."

File Uploads

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

builder.to_s      # => multipart body with file data
builder.boundary  # => the boundary string

Streaming IO Objects

require "stringio"

io = StringIO.new("CSV,data,here")

builder = Philiprehberger::Multipart.build do
  file :upload, io, filename: 'data.csv', content_type: 'text/csv'
end

builder.to_s  # => multipart body streamed from IO

MIME Type Detection

# Auto-detected from filename when content_type is not provided
builder = Philiprehberger::Multipart.build do
  file :doc, '/path/to/report.pdf'       # => application/pdf
  file :img, '/path/to/photo.jpg'        # => image/jpeg
  file :data, '/path/to/config.json'     # => application/json
end

# Direct lookup
Philiprehberger::Multipart::MimeTypes.lookup('photo.jpg')   # => "image/jpeg"
Philiprehberger::Multipart::MimeTypes.lookup('unknown.xyz')  # => "application/octet-stream"

Parsing Multipart Bodies

body = "--boundary\r\n" \
       "Content-Disposition: form-data; name=\"field\"\r\n" \
       "\r\n" \
       "value\r\n" \
       "--boundary--\r\n"

parts = Philiprehberger::Multipart.parse(body, content_type: 'multipart/form-data; boundary=boundary')

parts.first.name          # => :field
parts.first.body          # => "value"
parts.first.file?         # => false

Streaming Output

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

# Stream directly to a file or socket without buffering the full body
File.open('body.dat', 'wb') { |f| builder.write_to(f) }

# Content-Length for HTTP headers
builder.content_length  # => 1234
builder.headers         # => { "Content-Type" => "multipart/form-data; boundary=...", "Content-Length" => "1234" }

Post-Construction Part Lookup

builder = Philiprehberger::Multipart.build do
  field :name, 'Alice'
  file :avatar, '/path/to/photo.png'
end

# Look up a part by name (String or Symbol both work)
builder.part(:avatar)   # => #<Part name=:avatar ...>
builder.part('avatar')  # => same Part

# Tweak attributes after the fact — changes flow through to #to_s
builder.part('avatar').content_type = 'image/webp'

# Returns nil when no part matches
builder.part(:missing)  # => nil

Custom Boundary

builder = Philiprehberger::Multipart.build(boundary: 'my-boundary') do
  field :key, 'value'
end

builder.content_type  # => "multipart/form-data; boundary=my-boundary"

API

Method Description
Multipart.build(boundary: nil, &block) Build a multipart body using the DSL
Multipart.parse(body, content_type:) Parse an incoming multipart/form-data body
MimeTypes.lookup(filename) Look up MIME type from a filename extension
Builder#field(name, value) Add a text field
Builder#file(name, path_or_io, filename:, content_type:) Add a file from path or IO object
Builder#part(name) Look up the first part with a matching name (Symbol or String), or nil
Builder#to_s Render the multipart body as a string
Builder#content_type Content-Type header value with boundary
Builder#boundary The multipart boundary string
Builder#write_to(io) Stream the multipart body to an IO object
Builder#content_length Byte size of the body for Content-Length headers
Builder#headers Hash with Content-Type and Content-Length headers
Part#name The field name
Part#value The part value / body content
Part#body Alias for value
Part#filename The original filename (nil for text fields)
Part#content_type The MIME content type (nil for text fields)
Part#file? Whether this part is a file upload

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT