README.md

<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.