# `HTTP.FormData`
[🔗](https://github.com/gsmlg-dev/http_fetch/blob/v0.21.18/apps/http_core/lib/http/form_data.ex#L1)

HTTP form data and multipart/form-data encoding for file uploads.

This module provides a convenient API for building form submissions with support
for both URL-encoded forms and multipart file uploads. It automatically chooses
the appropriate encoding based on the presence of file fields.

## Encoding Selection

- **URL-encoded** (`application/x-www-form-urlencoded`): Used when form contains only text fields
- **Multipart** (`multipart/form-data`): Used when form contains file fields

## Features

- **File stream inputs**: Accepts `File.Stream` values when building multipart bodies
- **Automatic encoding**: Selects appropriate encoding based on content
- **Boundary generation**: Automatically generates unique multipart boundaries
- **Mixed content**: Support for both text fields and files in the same form

## Basic Usage

    # Simple form with text fields
    form = HTTP.FormData.new()
           |> HTTP.FormData.append_field("username", "john_doe")
           |> HTTP.FormData.append_field("email", "john@example.com")

    HTTP.fetch("https://api.example.com/signup", method: "POST", body: form)

## File Upload

    # Single file upload
    file_stream = File.stream!("document.pdf")
    form = HTTP.FormData.new()
           |> HTTP.FormData.append_field("title", "My Document")
           |> HTTP.FormData.append_file("document", "document.pdf", file_stream, "application/pdf")

    HTTP.fetch("https://api.example.com/upload", method: "POST", body: form)

## Multiple Files

    # Upload multiple files
    form = HTTP.FormData.new()
           |> HTTP.FormData.append_field("description", "Photos from vacation")
           |> HTTP.FormData.append_file("photo1", "beach.jpg", File.stream!("beach.jpg"), "image/jpeg")
           |> HTTP.FormData.append_file("photo2", "sunset.jpg", File.stream!("sunset.jpg"), "image/jpeg")

    HTTP.fetch("https://api.example.com/gallery", method: "POST", body: form)

## Content Types

When uploading files, you can specify the MIME type. If not provided, it defaults
to `"application/octet-stream"`:

    # With explicit content type
    form |> HTTP.FormData.append_file("image", "photo.jpg", stream, "image/jpeg")

    # With default content type
    form |> HTTP.FormData.append_file("data", "data.bin", stream)

# `form_part`

```elixir
@type form_part() ::
  {:field, String.t(), String.t()}
  | {:file, String.t(), String.t(), String.t(), File.Stream.t()}
  | {:file, String.t(), String.t(), String.t(), String.t()}
```

# `stream_envelope`

```elixir
@type stream_envelope() :: %{
  body: Enumerable.t(),
  content_length: non_neg_integer(),
  content_type: String.t(),
  boundary: String.t()
}
```

# `t`

```elixir
@type t() :: %HTTP.FormData{boundary: String.t() | nil, parts: [form_part()]}
```

# `append_field`

```elixir
@spec append_field(t(), String.t(), String.t()) :: t()
```

Adds a form field.

## Examples

    iex> HTTP.FormData.new() |> HTTP.FormData.append_field("name", "value")
    %HTTP.FormData{parts: [{:field, "name", "value"}], boundary: nil}

# `append_file`

```elixir
@spec append_file(
  t(),
  String.t(),
  String.t(),
  File.Stream.t() | String.t(),
  String.t()
) :: t()
```

Adds a file field for upload.

## Examples

    iex> file_stream = File.stream!("test.txt")
    iex> HTTP.FormData.new() |> HTTP.FormData.append_file("upload", "test.txt", file_stream)
    %HTTP.FormData{parts: [{:file, "upload", "test.txt", "text/plain", %File.Stream{}}], boundary: nil}

# `generate_boundary`

```elixir
@spec generate_boundary() :: String.t()
```

Generates a random boundary for multipart/form-data.

# `get_content_type`

```elixir
@spec get_content_type(t()) :: String.t()
```

Gets the appropriate Content-Type header for the form data.

# `new`

```elixir
@spec new() :: t()
```

Creates a new empty FormData struct.

## Examples

    iex> HTTP.FormData.new()
    %HTTP.FormData{parts: [], boundary: nil}

# `to_body`

```elixir
@spec to_body(t()) :: {:url_encoded, String.t()} | {:multipart, iodata(), String.t()}
```

Converts FormData to HTTP body content with appropriate encoding.

Returns {:url_encoded, body} for regular forms or {:multipart, body, boundary} for multipart.
The multipart body is returned as iodata for memory efficiency with large file uploads.

# `to_stream`

```elixir
@spec to_stream(t(), keyword()) :: {:ok, stream_envelope()} | {:error, term()}
```

Builds a lazy multipart envelope with an exact byte length.

Returns `{:ok, %{body: enumerable, content_length: bytes, content_type: type,
boundary: boundary}}`. Construction stats regular file paths but does not open
or read them. Enumeration opens one file at a time and emits binary chunks of
at most `:chunk_size` bytes (default 32,768; maximum 65,536). Halting enumeration
closes the current file. The existing `to_body/1` buffered API is unchanged.

For Fetch uploads, adapt the enumerable through `HTTP.Stream.from_enumerable/1`
and provide the matching length and content type:

    {:ok, multipart} = HTTP.FormData.to_stream(form)
    {:ok, upload} = HTTP.Stream.from_enumerable(multipart.body)
    HTTP.fetch(url,
      method: :post,
      body: upload,
      duplex: :half,
      content_type: multipart.content_type,
      headers: [{"Content-Length", Integer.to_string(multipart.content_length)}]
    )

File inputs must be local regular `File.Stream` paths with byte-preserving
modes. Default line-mode streams are reopened in binary chunks. Encoded,
compressed, remote-node and write-mode streams are rejected because their
output length cannot be obtained from file metadata alone. Field values and
in-memory file content must be binaries.

Keep files unchanged until upload completion. Fetch's fixed-length upload
checks reject underflow or overflow if a file's size changes. Creating the
envelope starts no producer; Fetch's upload stream owns enumeration and its
existing cancellation, failed-connection and owner-exit cleanup applies. The
serializer does not retry uploads.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
