Class: Philiprehberger::Multipart::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/multipart/builder.rb

Overview

DSL builder for constructing multipart/form-data bodies

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(boundary: nil) ⇒ Builder

Returns a new instance of Builder.

Parameters:

  • boundary (String, nil) (defaults to: nil)

    optional custom boundary string



17
18
19
20
# File 'lib/philiprehberger/multipart/builder.rb', line 17

def initialize(boundary: nil)
  @boundary = boundary || generate_boundary
  @parts = []
end

Instance Attribute Details

#boundaryString (readonly)

Returns the multipart boundary string.

Returns:

  • (String)

    the multipart boundary string



11
12
13
# File 'lib/philiprehberger/multipart/builder.rb', line 11

def boundary
  @boundary
end

#partsArray<Part> (readonly)

Returns the parts added to the builder.

Returns:

  • (Array<Part>)

    the parts added to the builder



14
15
16
# File 'lib/philiprehberger/multipart/builder.rb', line 14

def parts
  @parts
end

Instance Method Details

#content_lengthInteger

Calculate the byte size of the multipart body

Returns:

  • (Integer)

    the body size in bytes



121
122
123
124
# File 'lib/philiprehberger/multipart/builder.rb', line 121

def content_length
  size = @parts.sum { |part| part.to_s(@boundary).bytesize }
  size + "--#{@boundary}--\r\n".bytesize
end

#content_typeString

Return the Content-Type header value with boundary

Returns:

  • (String)


101
102
103
# File 'lib/philiprehberger/multipart/builder.rb', line 101

def content_type
  "multipart/form-data; boundary=#{@boundary}"
end

#field(name, value) ⇒ self

Add a text field

Parameters:

  • name (Symbol, String)

    the field name

  • value (String)

    the field value

Returns:

  • (self)


27
28
29
30
# File 'lib/philiprehberger/multipart/builder.rb', line 27

def field(name, value)
  @parts << Part.new(name, value.to_s)
  self
end

#field_namesArray<String>

List part names in the order they were added.

Duplicates are preserved (this is a list, not a set). The returned array is a fresh copy — mutating it does not affect the builder.

Returns:

  • (Array<String>)

    part names in insertion order



72
73
74
# File 'lib/philiprehberger/multipart/builder.rb', line 72

def field_names
  @parts.map { |p| p.name.to_s }
end

#file(name, path_or_io, filename: nil, content_type: nil) ⇒ self

Add a file field

Accepts either a file path (String) or an IO object (responds to :read). When passing an IO object, the ‘filename:` keyword is required. Content type is auto-detected from the filename when not provided.

Parameters:

  • name (Symbol, String)

    the field name

  • path_or_io (String, #read)

    the file path or IO object

  • filename (String, nil) (defaults to: nil)

    override filename (required for IO objects)

  • content_type (String, nil) (defaults to: nil)

    the MIME content type (auto-detected if nil)

Returns:

  • (self)

Raises:

  • (Error)

    if the file does not exist (path mode) or filename is missing (IO mode)



44
45
46
47
48
49
50
51
# File 'lib/philiprehberger/multipart/builder.rb', line 44

def file(name, path_or_io, filename: nil, content_type: nil)
  if path_or_io.respond_to?(:read)
    add_io_file(name, path_or_io, filename, content_type)
  else
    add_path_file(name, path_or_io, filename, content_type)
  end
  self
end

#headersHash

Return the headers hash for the request

Returns:

  • (Hash)


129
130
131
132
133
134
# File 'lib/philiprehberger/multipart/builder.rb', line 129

def headers
  {
    'Content-Type' => content_type,
    'Content-Length' => content_length.to_s
  }
end

#merge(other) ⇒ self

Append parts from another builder onto this one, preserving their existing ‘Part` objects so file contents are not re-encoded. The receiver’s boundary is kept; only ‘other`’s parts are copied.

Parameters:

  • other (Builder)

    the builder whose parts should be appended

Returns:

  • (self)

Raises:

  • (Error)

    if ‘other` is not a `Builder`



83
84
85
86
87
88
# File 'lib/philiprehberger/multipart/builder.rb', line 83

def merge(other)
  raise Error, 'merge requires a Builder' unless other.is_a?(Builder)

  @parts.concat(other.parts)
  self
end

#part(name) ⇒ Part?

Look up the first part previously added with the given name.

Allows post-construction tweaks, e.g. ‘builder.part(’avatar’).content_type = ‘image/webp’‘. String and Symbol lookups are equivalent.

Parameters:

  • name (Symbol, String)

    the field name to look up

Returns:

  • (Part, nil)

    the first matching Part, or nil if absent



61
62
63
64
# File 'lib/philiprehberger/multipart/builder.rb', line 61

def part(name)
  needle = name.to_s
  @parts.find { |p| p.name.to_s == needle }
end

#to_sString

Render the complete multipart body as a string

Returns:

  • (String)


93
94
95
96
# File 'lib/philiprehberger/multipart/builder.rb', line 93

def to_s
  body = @parts.map { |part| part.to_s(@boundary) }.join
  "#{body}--#{@boundary}--\r\n"
end

#write_to(io) ⇒ #write

Stream the multipart body to an IO object

Writes each part directly to the IO without building the full body string in memory. Useful for large file uploads.

Parameters:

  • io (#write)

    the IO object to write to

Returns:

  • (#write)

    the IO object



112
113
114
115
116
# File 'lib/philiprehberger/multipart/builder.rb', line 112

def write_to(io)
  @parts.each { |part| io.write(part.to_s(@boundary)) }
  io.write("--#{@boundary}--\r\n")
  io
end