Skip to content

Commit 27629cf

Browse files
committed
docs: Enhance module documentation and README
Comprehensively update documentation across all modules to improve developer experience: - Add Development section to README.md with: - Code quality tools (Credo and Dialyzer) usage - Testing, formatting, and documentation generation commands - Clear requirements section for Elixir 1.18+ and Erlang OTP - Expand @moduledoc for all 10 core modules with: - Comprehensive feature overviews - Architecture and design pattern explanations - Rich usage examples for common scenarios - Implementation details and best practices - Enhanced module-specific documentation: - HTTP: Add streaming behavior, telemetry events, quick start - HTTP.Promise: Document Promise pattern with chaining examples - HTTP.Request: Explain :httpc.request/4 argument mapping - HTTP.Response: Document streaming vs buffered responses - HTTP.Headers: Add case-insensitive operations and parsing - HTTP.FormData: Document encoding selection and streaming uploads - HTTP.AbortController: Add "How It Works" with thread-safety details - HTTP.FetchOptions: Categorize and document all available options - HTTP.Telemetry: List all events with measurements and metadata - HTTPFetch.Application: Document supervision tree structure - Achieve 100% module documentation coverage - Verify clean documentation build with mix docs (no warnings/errors)
1 parent 8b26c50 commit 27629cf

File tree

11 files changed

+724
-18
lines changed

11 files changed

+724
-18
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,73 @@ The library handles:
243243
- Invalid URLs
244244
- Cancelled requests
245245

246+
## Development
247+
248+
This project uses several code quality tools to maintain high standards:
249+
250+
### Code Quality Tools
251+
252+
**Credo** - Static code analysis to enforce Elixir style guidelines and identify code smells:
253+
254+
```bash
255+
# Run standard checks
256+
mix credo
257+
258+
# Run with strict mode (includes readability checks)
259+
mix credo --strict
260+
261+
# Explain a specific issue
262+
mix credo explain <issue_category>
263+
```
264+
265+
**Dialyzer** - Static type analysis to catch type errors and inconsistencies:
266+
267+
```bash
268+
# Run type checking
269+
mix dialyzer
270+
271+
# Generate/rebuild PLT (first time setup, takes 2-3 minutes)
272+
mix dialyzer --plt
273+
```
274+
275+
**ExDoc** - Generate comprehensive documentation:
276+
277+
```bash
278+
# Generate HTML documentation
279+
mix docs
280+
281+
# View generated docs
282+
open doc/index.html
283+
```
284+
285+
### Running Tests
286+
287+
```bash
288+
# Run all tests
289+
mix test
290+
291+
# Run specific test file
292+
mix test test/http_test.exs
293+
294+
# Run with coverage
295+
mix test --cover
296+
```
297+
298+
### Code Formatting
299+
300+
```bash
301+
# Format all code
302+
mix format
303+
304+
# Check formatting without changes
305+
mix format --check-formatted
306+
```
307+
308+
## Requirements
309+
310+
- Elixir 1.18+ (for built-in `JSON` module support)
311+
- Erlang OTP with `:inets`, `:ssl`, `:public_key` applications
312+
246313
## License
247314

248315
MIT License

lib/http.ex

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,75 @@
11
defmodule HTTP do
22
@moduledoc """
3-
A module simulating the web browser's Fetch API in Elixir, using :httpc as the foundation.
4-
Provides HTTP.Request, HTTP.Response, HTTP.Promise and a global-like fetch function with asynchronous
5-
capabilities and an AbortController for request cancellation.
3+
A browser-like HTTP fetch API for Elixir, built on Erlang's `:httpc` module.
4+
5+
This module provides a modern, Promise-based HTTP client interface similar to the
6+
browser's `fetch()` API. It supports asynchronous requests, streaming, request
7+
cancellation, and comprehensive telemetry integration.
8+
9+
## Features
10+
11+
- **Async by default**: All requests use Task.Supervisor with `async_nolink/4`
12+
- **Automatic streaming**: Responses >5MB or with unknown Content-Length automatically stream
13+
- **Request cancellation**: Via `HTTP.AbortController` for aborting in-flight requests
14+
- **Promise chaining**: JavaScript-like promise interface with `then/3` support
15+
- **Telemetry integration**: Comprehensive event emission for monitoring and observability
16+
- **Zero external dependencies**: Uses only Erlang/OTP built-in modules (except telemetry)
17+
18+
## Quick Start
19+
20+
# Simple GET request
21+
{:ok, response} =
22+
HTTP.fetch("https://jsonplaceholder.typicode.com/posts/1")
23+
|> HTTP.Promise.await()
24+
25+
# Parse JSON response
26+
{:ok, json} = HTTP.Response.json(response)
27+
28+
# POST with JSON body
29+
{:ok, response} =
30+
HTTP.fetch("https://api.example.com/posts", [
31+
method: "POST",
32+
headers: %{"Content-Type" => "application/json"},
33+
body: JSON.encode!(%{title: "Hello", body: "World"})
34+
])
35+
|> HTTP.Promise.await()
36+
37+
## Architecture
38+
39+
The library is structured around these core modules:
40+
41+
- `HTTP` - Main entry point with the `fetch/2` function
42+
- `HTTP.Promise` - Promise wrapper around Tasks for async operations
43+
- `HTTP.Request` - Request configuration struct
44+
- `HTTP.Response` - Response struct with JSON/text parsing helpers
45+
- `HTTP.Headers` - Header manipulation utilities
46+
- `HTTP.FormData` - Multipart/form-data encoding with file upload support
47+
- `HTTP.AbortController` - Request cancellation mechanism
48+
- `HTTP.FetchOptions` - Options processing and validation
49+
- `HTTP.Telemetry` - Telemetry event emission for monitoring
50+
51+
## Streaming Behavior
52+
53+
Responses are automatically streamed when:
54+
55+
- Content-Length > 5MB
56+
- Content-Length header is missing/unknown
57+
58+
Streaming responses have `body: nil` and `stream: pid` in the Response struct.
59+
Use `HTTP.Response.read_all/1` or `HTTP.Response.write_to/2` to consume streams.
60+
61+
## Telemetry Events
62+
63+
All events use the `[:http_fetch, ...]` prefix:
64+
65+
- `[:http_fetch, :request, :start]` - Request initiated
66+
- `[:http_fetch, :request, :stop]` - Request completed
67+
- `[:http_fetch, :request, :exception]` - Request failed
68+
- `[:http_fetch, :streaming, :start]` - Streaming started
69+
- `[:http_fetch, :streaming, :chunk]` - Stream chunk received
70+
- `[:http_fetch, :streaming, :stop]` - Streaming completed
71+
72+
See `HTTP.Telemetry` for detailed event documentation.
673
"""
774

875
alias HTTP.Request

lib/http/abort_controller.ex

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,80 @@
11
defmodule HTTP.AbortController do
22
use Agent
33

4-
@type state :: %{request_id: pid() | nil, signal_ref: reference(), aborted: boolean()}
5-
64
@moduledoc """
7-
Provides request cancellation functionality similar to the browser's AbortController.
5+
Request cancellation mechanism similar to the browser's AbortController API.
6+
7+
This module provides a way to abort in-flight HTTP requests using an Agent-based
8+
controller. It's designed to work with the `HTTP.fetch/2` function via the
9+
`:signal` option.
10+
11+
## How It Works
12+
13+
1. Create an AbortController before making the request
14+
2. Pass the controller to `HTTP.fetch/2` via the `:signal` option
15+
3. Call `abort/1` on the controller to cancel the request
16+
4. The awaiting Promise will receive an error result
17+
18+
## Basic Usage
19+
20+
# Create a controller
21+
controller = HTTP.AbortController.new()
22+
23+
# Start a long-running request with the controller
24+
promise = HTTP.fetch("https://httpbin.org/delay/10",
25+
signal: controller,
26+
options: [timeout: 20_000]
27+
)
28+
29+
# Abort the request (e.g., after 2 seconds)
30+
:timer.sleep(2000)
31+
HTTP.AbortController.abort(controller)
32+
33+
# The awaited promise will return an error
34+
case HTTP.Promise.await(promise) do
35+
{:error, reason} ->
36+
IO.puts("Request was aborted: " <> inspect(reason))
37+
response ->
38+
IO.puts("Request completed before abort")
39+
end
40+
41+
## Advanced Usage
42+
43+
# Abort from another process
44+
controller = HTTP.AbortController.new()
45+
46+
# Start request in background
47+
Task.start(fn ->
48+
promise = HTTP.fetch("https://slow-api.example.com", signal: controller)
49+
result = HTTP.Promise.await(promise)
50+
IO.inspect(result)
51+
end)
52+
53+
# Abort from main process after some condition
54+
:timer.sleep(1000)
55+
if some_condition?() do
56+
HTTP.AbortController.abort(controller)
57+
end
58+
59+
## Implementation Details
60+
61+
- Uses Elixir's `Agent` for state management
62+
- Registers with a `Registry` for process tracking
63+
- Calls `:httpc.cancel_request/1` internally to abort the request
64+
- Thread-safe and can be called from any process
65+
- Idempotent - calling `abort/1` multiple times is safe
66+
67+
## State Management
68+
69+
The controller maintains the following state:
70+
71+
- `request_id` - PID of the active `:httpc` request (set automatically)
72+
- `signal_ref` - Unique reference for registry lookup
73+
- `aborted` - Boolean flag indicating abort status
874
"""
975

76+
@type state :: %{request_id: pid() | nil, signal_ref: reference(), aborted: boolean()}
77+
1078
@doc """
1179
Starts a new AbortController agent.
1280
Returns `{:ok, pid}` of the agent.

lib/http/fetch_options.ex

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,85 @@
11
defmodule HTTP.FetchOptions do
22
@moduledoc """
3-
Processes fetch options for HTTP requests, supporting flat map, keyword list,
4-
and HTTP.FetchOptions struct formats. Handles conversion to :httpc.request arguments.
3+
Options processing and validation for `HTTP.fetch/2` requests.
4+
5+
This module handles the conversion of various option formats (maps, keyword lists,
6+
structs) into structured options for `:httpc.request/4`. It provides a flexible
7+
API that supports both simple and advanced HTTP client configurations.
8+
9+
## Options Categories
10+
11+
Options are divided into two main categories:
12+
13+
1. **HTTP Options** (3rd argument to `:httpc.request/4`) - Request-specific settings:
14+
- `timeout` - Request timeout in milliseconds
15+
- `connect_timeout` - Connection timeout in milliseconds
16+
- `ssl` - SSL/TLS options
17+
- `autoredirect` - Follow redirects automatically
18+
- `proxy_auth` - Proxy authentication
19+
- `version` - HTTP version
20+
- `relaxed` - Relaxed parsing mode
21+
22+
2. **Client Options** (4th argument to `:httpc.request/4`) - Client behavior settings:
23+
- `sync` - Synchronous/asynchronous mode (default: false)
24+
- `stream` - Response streaming configuration
25+
- `body_format` - Response body format (:string or :binary)
26+
- `full_result` - Return full HTTP response
27+
- `headers_as_is` - Preserve header case
28+
- `socket_opts` - Socket-level options
29+
- `receiver` - Custom receiver process
30+
- `ipv6_host_with_brackets` - IPv6 host formatting
31+
32+
## Basic Usage
33+
34+
# Simple options as keyword list
35+
HTTP.fetch("https://api.example.com", [
36+
method: "POST",
37+
headers: %{"Content-Type" => "application/json"},
38+
timeout: 10_000
39+
])
40+
41+
# Complex options with HTTP and client settings
42+
HTTP.fetch("https://api.example.com", [
43+
method: "GET",
44+
timeout: 5_000,
45+
connect_timeout: 2_000,
46+
ssl: [verify: :verify_peer],
47+
body_format: :binary
48+
])
49+
50+
## Advanced Configuration
51+
52+
# Using options and opts keywords for fine control
53+
HTTP.fetch("https://api.example.com", [
54+
method: "POST",
55+
body: "data",
56+
# Request-specific options (3rd arg to :httpc.request)
57+
options: [
58+
timeout: 10_000,
59+
connect_timeout: 5_000
60+
],
61+
# Client-specific options (4th arg to :httpc.request)
62+
opts: [
63+
sync: false,
64+
body_format: :binary
65+
]
66+
])
67+
68+
## Flat vs Structured Options
69+
70+
The module supports both flat and structured option formats:
71+
72+
# Flat format (recommended for simplicity)
73+
[method: "POST", timeout: 5_000, body_format: :binary]
74+
75+
# Structured format (for explicit control)
76+
[
77+
method: "POST",
78+
options: [timeout: 5_000],
79+
opts: [body_format: :binary]
80+
]
81+
82+
Both formats are equivalent; the module automatically categorizes options.
583
"""
684

785
defstruct method: :get,

lib/http/form_data.ex

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
11
defmodule HTTP.FormData do
22
@moduledoc """
3-
Handles HTTP form data and multipart/form-data encoding for file uploads.
4-
Supports both regular form data and multipart uploads with streaming file support.
3+
HTTP form data and multipart/form-data encoding for file uploads.
4+
5+
This module provides a convenient API for building form submissions with support
6+
for both URL-encoded forms and multipart file uploads. It automatically chooses
7+
the appropriate encoding based on the presence of file fields.
8+
9+
## Encoding Selection
10+
11+
- **URL-encoded** (`application/x-www-form-urlencoded`): Used when form contains only text fields
12+
- **Multipart** (`multipart/form-data`): Used when form contains file fields
13+
14+
## Features
15+
16+
- **Streaming file uploads**: Efficiently upload large files using `File.Stream`
17+
- **Automatic encoding**: Selects appropriate encoding based on content
18+
- **Boundary generation**: Automatically generates unique multipart boundaries
19+
- **Mixed content**: Support for both text fields and files in the same form
20+
21+
## Basic Usage
22+
23+
# Simple form with text fields
24+
form = HTTP.FormData.new()
25+
|> HTTP.FormData.append_field("username", "john_doe")
26+
|> HTTP.FormData.append_field("email", "[email protected]")
27+
28+
HTTP.fetch("https://api.example.com/signup", method: "POST", body: form)
29+
30+
## File Upload
31+
32+
# Single file upload
33+
file_stream = File.stream!("document.pdf")
34+
form = HTTP.FormData.new()
35+
|> HTTP.FormData.append_field("title", "My Document")
36+
|> HTTP.FormData.append_file("document", "document.pdf", file_stream, "application/pdf")
37+
38+
HTTP.fetch("https://api.example.com/upload", method: "POST", body: form)
39+
40+
## Multiple Files
41+
42+
# Upload multiple files
43+
form = HTTP.FormData.new()
44+
|> HTTP.FormData.append_field("description", "Photos from vacation")
45+
|> HTTP.FormData.append_file("photo1", "beach.jpg", File.stream!("beach.jpg"), "image/jpeg")
46+
|> HTTP.FormData.append_file("photo2", "sunset.jpg", File.stream!("sunset.jpg"), "image/jpeg")
47+
48+
HTTP.fetch("https://api.example.com/gallery", method: "POST", body: form)
49+
50+
## Content Types
51+
52+
When uploading files, you can specify the MIME type. If not provided, it defaults
53+
to `"application/octet-stream"`:
54+
55+
# With explicit content type
56+
form |> HTTP.FormData.append_file("image", "photo.jpg", stream, "image/jpeg")
57+
58+
# With default content type
59+
form |> HTTP.FormData.append_file("data", "data.bin", stream)
560
"""
661

762
defstruct parts: [],

0 commit comments

Comments
 (0)