zenwave 0.1.0

Ergonomic HTTP client framework.
Documentation
# Zenwave

[![crates.io](https://img.shields.io/crates/v/zenwave.svg)](https://crates.io/crates/zenwave)
[![Documentation](https://docs.rs/zenwave/badge.svg)](https://docs.rs/zenwave)
[![License](https://img.shields.io/crates/l/zenwave.svg)](LICENSE)
[![Coverage](https://img.shields.io/codecov/c/github/zen-rs/zenwave?logo=codecov)](https://app.codecov.io/gh/zen-rs/zenwave)



Zenwave is an ergonomic, full-featured HTTP client framework for Rust. It exposes a modern,
middleware-friendly API that works on both native targets (Tokio + Hyper on Linux/Windows, Apple's
URLSession on iOS/tvOS/watchOS/macOS) and browser/Cloudflare Workers targets through the Fetch API.

## Why Zenwave?

- **Ergonomic requests** – convenience helpers (`get`, `post`, …) and a fluent `RequestBuilder`.
- **Opt-in middleware** – add redirect following, cookie storage, OAuth2 refresh, or redirects only when you
  need it.
- **Streaming bodies** – handle large uploads/downloads or upgrade to SSE without buffering.
- **HTTP caching** – drop-in middleware honors `Cache-Control`, `Expires`, `ETag`, and
  `Last-Modified` to avoid redundant network hops.
- **Native timers** – enforce per-request deadlines with high-precision timers on every supported
  platform via a simple `.timeout(...)` helper.
- **Proxy aware** – honor `HTTP(S)_PROXY`/`NO_PROXY` or define custom SOCKS/HTTP proxies when using the Hyper or curl backends.
- **WebSocket ready** – one API that works natively and in WASM.
- **Pluggable backends** – Hyper on general native targets, URLSession on Apple platforms, libcurl
  when you want small binaries, and Fetch on wasm, all behind the same `zenwave::client()` interface.

## Quick Start

```rust
use zenwave::{get, ResponseExt};

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let response = get("https://example.com/").await?;
    let text = response.into_string().await?;
    println!("{text}");
    Ok(())
}
```

The `ResponseExt` trait provides the `into_string`, `into_json`, `into_bytes`, and `into_sse` helpers
you will see throughout the API.

## Examples

Run the shipped samples with `cargo run --example <name>`:

- `basic_get` – one-off GET request parsed into a typed response.
- `custom_client` – compose middleware, send JSON, and read a typed response body.
- `websocket_echo` – connect to a public echo server using the cross-platform WebSocket client.

Feel free to copy these examples as starting points for your own projects.

## Building richer clients

```rust
use std::time::Duration;

use serde::{Deserialize, Serialize};
use zenwave::{self, Cache, Client, OAuth2ClientCredentials};

#[derive(Serialize)]
struct MessageRequest<'a> {
    message: &'a str,
}

#[derive(Deserialize)]
struct EchoResponse {
    json: MessageResponse,
}

#[derive(Deserialize)]
struct MessageResponse {
    message: String,
}

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let token = std::env::var("ZENWAVE_TOKEN").unwrap_or_else(|_| "demo-token".into());

    // Compose only the middleware you need.
    let client = zenwave::client()
        .timeout(Duration::from_secs(2))
        .enable_cache()
        .with(OAuth2ClientCredentials::new(
            "https://auth.example.com/token",
            "client-id",
            "client-secret",
        ))
        .follow_redirect()
        .enable_cookie()
        .bearer_auth(token);

    let echo: EchoResponse = client
        .post("https://httpbin.org/post")
        .header("x-request-id", "demo-request")
        .json_body(&MessageRequest { message: "hello" })?
        .json()
        .await?;

    println!("{}", echo.json.message);
    Ok(())
}
```

You can also call `.basic_auth` or `.with(custom_middleware)` to plug in your own behavior. Every
request builder supports `.header`, `.bearer_auth`, `.basic_auth`, `.json_body`, `.bytes_body`, and
body readers (`.json()`, `.string()`, `.bytes()`, `.form()`, `.sse()`).

Timeouts are middleware too. Calling `.timeout(Duration::from_secs(2))` wraps the client in a
native-executor-backed timer so every subsequent request automatically fails with a
`504 Gateway Timeout` when the deadline is exceeded.

## Proxy configuration (native Hyper / curl backends)

Zenwave can route requests through HTTP or SOCKS proxies by reading the
standard `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` variables or
by constructing a matcher manually. Both the Hyper and libcurl-native backends
honor the same configuration:

```rust
use zenwave::{self, Proxy};

fn main() {
    // Inherit proxy settings from the environment (`*_PROXY` / `NO_PROXY`).
    let proxy = Proxy::from_env();
    let client = zenwave::client_with_proxy(proxy);

    // Or build one manually. Supports http, socks4, socks4a, socks5, and socks5h schemes.
    let custom = Proxy::builder()
        .http("http://corp-proxy:8080")
        .no_proxy("internal.example.com")
        .build();
    let mut custom_client = zenwave::client_with_proxy(custom);

    // Clients returned by `zenwave::client()` can also be swapped afterwards:
    let mut swapped = zenwave::client().proxy(Proxy::from_system());
}
```

Only the Hyper and curl backends currently honor proxies. HTTP CONNECT proxies
(`http://` / `https://`) and SOCKS4/5 proxies (`socks4[a]`, `socks5[h]`) are supported.
The Apple (`apple-backend`) and Web (`wasm32`) backends do not expose proxy
APIs, so helper functions such as `client_with_proxy` or `.proxy(...)` are not
compiled when those backends are selected as the default.

## Large downloads with resume

Native targets get an ergonomic helper for writing large responses to disk without buffering into
memory. Any request builder can call `download_to_path` to stream the body into a file. When the
file already exists Zenwave automatically issues a `Range` request and appends only the missing
bytes, so interrupted transfers can resume without starting from scratch.

```rust
use zenwave::Client;

# async fn example() -> zenwave::Result<()> {
let client = zenwave::client();
let report = client
    .get("https://example.com/big.iso")
    .download_to_path("big.iso")
    .await?;

println!(
    "Resumed from {} bytes and wrote {} bytes ({} total)",
    report.resumed_from,
    report.bytes_written,
    report.total_bytes()
);
# Ok(())
# }
```

If you need to opt out of resume logic you can call `download_to_path_with` and pass
`DownloadOptions { resume_existing: false }`. Both methods return a `DownloadReport` so you can log
how much data was appended and what now resides on disk. This helper is currently available on
non-wasm targets where direct filesystem access exists.

## Streaming uploads

Use `file_body` to upload large files directly from disk without buffering, `reader_body` to wrap
any `AsyncRead`, or `stream_body` to hook up custom chunk producers. Each helper integrates with
Tokio so uploads backpressure naturally with the network stack.

```rust
# async fn example() -> zenwave::Result<()> {
use zenwave::client;
use async_fs::File;

let mut client = client();
let response = client
    .post("https://example.com/upload")
    .file_body("video.mp4")
    .await?
    .await?;
assert!(response.status().is_success());

let mut stream_client = client();
let file = File::open("log.txt").await?;
let response = stream_client
    .post("https://example.com/logs")
    .reader_body(file, None)
    .await?;
assert!(response.status().is_success());
# Ok(())
# }
```

## HTTP cache middleware

Call `.enable_cache()` to enable RFC-compliant client-side caching. The middleware caches
successful GET responses when permitted by `Cache-Control`/`Expires`, automatically injects
validators for stale entries (`If-None-Match`, `If-Modified-Since`), and serves `304 Not Modified`
responses straight from memory. Requests with `Authorization` headers are skipped unless the
response explicitly declares itself `public`. Because it is implemented as middleware you can keep
it for native builds only or combine it with other layers as needed.

## Persistent cookie store

Call `.enable_persistent_cookie()` to transparently load and save cookies between runs on native
targets. Zenwave automatically picks a cache file under your platform's local data directory using
the name `zenwave_cookie_store_<crate_name>.json`, so crates only share cookies with themselves by
default. You can also fully control the path via `CookieStore::persistent_with_path` if you want to
sync cookies across binaries.

## OAuth2 client credentials

Use `OAuth2ClientCredentials::new(token_url, client_id, client_secret)` to automatically obtain and
refresh bearer tokens. The middleware performs the client credentials flow against the configured
token endpoint, caches responses until they near expiration, and injects the `Authorization` header
for every outgoing request. Call `.with_scope("scope1 scope2")` or `.with_audience("api")` if your
provider requires additional parameters.

## Web & Cloudflare Workers

Zenwave targets both `wasm32` and native platforms. On wasm it relies on `web_sys::Request`/`Fetch`,
so it works in browsers and Cloudflare Workers without extra glue code. The API is identical, so
sharing code between targets is straightforward.

## Apple platforms

By default Apple targets (iOS, iPadOS, tvOS, watchOS, macOS) also use the Hyper backend. There is an
experimental `apple-backend` feature that swaps Hyper out for `URLSession`, which satisfies
watchOS/App Store restrictions but currently auto-follows redirects and auto-manages cookies. The
two middleware tests that asserted “no redirect / no automatic cookies” are skipped whenever the
`apple-backend` feature is enabled. Until the URLSession backend is stabilized, we recommend keeping
the default Hyper backend on Apple. If you still want to opt in:

```toml
zenwave = { version = "0.1.0", features = ["apple-backend"] }
```

## Curl backend

Many Linux distributions (and some embedded platforms) ship a system libcurl. Zenwave can reuse it
to avoid bundling Hyper/OpenSSL and shrink your binary. Disable the default features and enable the
curl backend:

```toml
[dependencies]
zenwave = { version = "0.1.0", default-features = false, features = ["curl-backend"] }
```

You still get the same middleware API; the only difference is which backend transports the bytes.

## WebSocket support

The `zenwave::websocket` module offers a cross-platform WebSocket client that hides the details of
`async-tungstenite` or `web_sys::WebSocket`. Connecting to an endpoint looks like:

```rust
use zenwave::websocket::{self, WebSocketMessage};

#[async_std::main]
async fn main() -> zenwave::Result<()> {
    let mut socket = websocket::connect("wss://echo.websocket.events").await?;
    socket.send_text("hello").await?;

    if let Some(WebSocketMessage::Text(text)) = socket.recv().await? {
        println!("Received: {text}");
    }

    socket.close().await
}
```

You can also split a connection to drive sending and receiving from different tasks:

```rust
let socket = websocket::connect("wss://echo.websocket.events").await?;
let (sender, receiver) = socket.split();

// `send` serializes to JSON by default; use `send_text` for raw text frames.
sender.send(&MyPayload { message: "hello" }).await?;
if let Some(reply) = receiver.recv().await? {
    println!("Got reply: {:?}", reply);
}
```

## Installation

Add Zenwave to your `Cargo.toml`. The default configuration uses the Hyper backend with rustls TLS:

```toml
[dependencies]
zenwave = { version = "0.1.0" }
```

For browser/Workers builds, no special configuration is needed - Zenwave automatically uses the
built-in web backend (Fetch API) on wasm32 targets:

```toml
# For wasm32 targets, default features are ignored and the web backend is used automatically
zenwave = { version = "0.1.0" }
```

### Feature flags

#### Backend Selection (native platforms only)

On wasm32 targets, the built-in web backend is always used automatically. No backend selection
is needed or available.

On native platforms, you can choose from:

- `hyper-backend` (default) – Hyper with async-io/async-net. The recommended choice for most use cases.
- `curl-backend` – libcurl-based backend with built-in proxy support. Good for platforms with system libcurl.
- `apple-backend` – experimental URLSession backend for Apple platforms (macOS/iOS).

#### TLS Selection (hyper-backend only)

- `rustls` (default) – pure-Rust TLS implementation. Cross-platform and secure.
- `native-tls` – uses the platform's native TLS (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows).

Only one TLS feature can be enabled at a time. These features only apply to `hyper-backend`;
other backends have their own TLS implementations.

#### Other Features

- `proxy` – enables proxy support (automatically enabled with `curl-backend`).

### Example configurations

```toml
# Use curl backend instead of hyper
zenwave = { version = "0.1.0", default-features = false, features = ["curl-backend"] }

# Use hyper with native-tls instead of rustls
zenwave = { version = "0.1.0", default-features = false, features = ["hyper-backend", "native-tls"] }

# Use Apple's native URLSession on macOS/iOS
zenwave = { version = "0.1.0", default-features = false, features = ["apple-backend"] }
```

## License

MIT License