<p align="center">
<img src="assets/multipart_ex.svg" alt="multipart_ex" width="400" />
</p>
# MultipartEx
<p align="center">
<a href="https://hex.pm/packages/multipart_ex"><img alt="Hex.pm" src="https://img.shields.io/hexpm/v/multipart_ex.svg"></a>
<a href="https://hexdocs.pm/multipart_ex"><img alt="HexDocs" src="https://img.shields.io/badge/docs-hexdocs-blue.svg"></a>
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg"></a>
</p>
Client-agnostic `multipart/form-data` construction for Elixir.
`multipart_ex` builds multipart payloads from plain forms or manual `%Multipart{}`
and `%Multipart.Part{}` structs, keeps file inputs explicit, and emits either
iodata or streams depending on the parts involved.
## Installation
Add `multipart_ex` to your dependencies:
```elixir
def deps do
[
{:multipart_ex, "~> 0.1.0"}
]
end
```
The optional `mime` dependency improves `Content-Type` inference for file parts.
## Quick Start
```elixir
form = %{
"name" => "Ada",
"avatar" => {:path, "/tmp/avatar.png"},
"meta" => %{"tags" => ["engineer", "tester"]}
}
multipart = Multipart.from_form(form)
headers = Multipart.Encoder.headers(multipart)
{content_type, body} = Multipart.Encoder.encode(multipart)
```
If every part is in memory, `body` is iodata. If any part is streamed, `body`
is a stream.
## Core Features
- Explicit file handling. Bare strings are never treated as paths.
- Flexible serialization for nested maps and lists.
- Ordered pair lists stay ordered, including duplicate keys.
- Manual `%Multipart{}` and embedded `%Multipart.Part{}` values are supported.
- Boundaries are validated and remain stable even for manually-built structs
without a boundary.
- Part headers and disposition parameters reject control characters to prevent
multipart header injection.
## File Inputs
Use one of the explicit file shapes:
```elixir
%{avatar: {:path, "/tmp/avatar.png"}}
%{avatar: {:content, File.read!("/tmp/avatar.png"), "avatar.png"}}
%{avatar: {"avatar.png", "raw-bytes"}}
%{avatar: {"avatar.png", "raw-bytes", "image/png"}}
%{avatar: {"avatar.png", "raw-bytes", "image/png", [{"x-upload-id", "123"}]}}
```
## Serialization
`Multipart.Form.serialize/2` supports three strategies:
```elixir
Multipart.Form.serialize(%{user: %{name: "Sam"}}, strategy: :bracket)
# => [{"user[name]", "Sam"}]
Multipart.Form.serialize(%{user: %{name: "Sam"}}, strategy: :dot)
# => [{"user.name", "Sam"}]
Multipart.Form.serialize(%{name: "Sam"}, strategy: :flat)
# => [{"name", "Sam"}]
```
List handling for dot strategy:
```elixir
Multipart.Form.serialize(%{roles: ["admin", "staff"]}, strategy: :dot)
# => [{"roles", "admin"}, {"roles", "staff"}]
Multipart.Form.serialize(%{roles: ["admin", "staff"]},
strategy: :dot,
list_format: :index
)
# => [{"roles[0]", "admin"}, {"roles[1]", "staff"}]
```
Nil handling:
```elixir
Multipart.Form.serialize(%{name: nil, age: "10"}, nil: :skip)
Multipart.Form.serialize(%{name: nil, age: "10"}, nil: :empty)
```
Ordered pair lists are preserved exactly:
```elixir
Multipart.Form.serialize([{"beta", "2"}, {"alpha", "1"}, {"beta", "3"}])
# => [{"beta", "2"}, {"alpha", "1"}, {"beta", "3"}]
```
Struct values are treated as leaves. This keeps scalar structs like `Date`
intact and lets `%Multipart.Part{}` values pass through to `to_parts/2`.
## Manual Parts And Multiparts
You can embed parts directly in a form:
```elixir
form = %{
metadata: "visible",
attachment: Multipart.Part.file("attachment", {"report.txt", "hello"})
}
multipart = Multipart.from_form(form)
```
Or assemble the multipart payload yourself:
```elixir
parts = [
Multipart.Part.form("name", "Ada"),
Multipart.Part.file("avatar", {:path, "/tmp/avatar.png"})
]
multipart = %Multipart{parts: parts}
{content_type, body} = Multipart.Encoder.encode(multipart)
```
If a manual `%Multipart{}` omits the boundary, the library derives a stable,
validated boundary from the struct so headers and body stay consistent.
## Content Length
`Multipart.Encoder.content_length/1` returns `{:ok, length}` when every part
size is known, otherwise `:unknown`.
```elixir
case Multipart.Encoder.content_length(multipart) do
{:ok, length} -> [{"content-length", Integer.to_string(length)}]
:unknown -> []
end
```
`Multipart.Encoder.headers/1` includes `content-length` automatically when it
can be computed.
## Adapters
### Finch
```elixir
{headers, {:stream, body}} = Multipart.Adapter.Finch.build(form)
Finch.build(:post, "https://example.com/upload", headers, body)
```
### Req
```elixir
opts = Multipart.Adapter.Req.request_opts(form)
Req.post!("https://example.com/upload", opts)
```
Or attach via a step:
```elixir
step = Multipart.Adapter.Req.step(form)
request = step.(%{headers: []})
```
## Safety Notes
- Multipart boundaries are validated against a conservative RFC 2046-safe
character set.
- Header names, header values, field names, and filenames reject control
characters.
- Bare strings are treated as field values, never as filesystem paths.
## Documentation
Generate local docs with:
```bash
mix docs
```
HexDocs navigation includes the README, focused guides under `guides/`, grouped
API modules, and the changelog.