rust_api_calling 1.0.0

A clean, idiomatic Rust HTTP client library with a single core request function and convenient GET/POST wrappers. Features builder pattern configuration, typed responses via serde generics, automatic session/cookie/XSRF management, and structured logging.
Documentation
# rust_api_calling

[![Crates.io](https://img.shields.io/crates/v/rust_api_calling.svg)](https://crates.io/crates/rust_api_calling)
[![Documentation](https://docs.rs/rust_api_calling/badge.svg)](https://docs.rs/rust_api_calling)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A clean, idiomatic Rust HTTP client library with **one core function** that handles all HTTP communication. Built on top of [reqwest](https://crates.io/crates/reqwest) with automatic JSON serialization via [serde](https://crates.io/crates/serde).

## Features

- **Single core function**`make_request` handles everything; `get()` and `post()` are thin wrappers
- **Typed responses** — Deserialize JSON directly into your Rust structs via generics
- **Builder pattern** — Configure the client once with `ApiClient::builder()`, reuse everywhere
- **Base URL support** — Set it once, then use relative paths like `"/users"`
- **Session management** — Automatic XSRF token and cookie handling across requests
- **Per-request overrides** — Custom timeout, headers, and bearer token per call via `RequestConfig`
- **Strongly-typed errors**`ApiError` enum with clear variants instead of magic error codes
- **Structured logging** — Built-in [tracing]https://crates.io/crates/tracing integration
- **Async/await** — Powered by [tokio]https://crates.io/crates/tokio

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
rust_api_calling = "0.1.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
```

## Quick Start

```rust
use rust_api_calling::{ApiClient, ApiResponse};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Post {
    id: u32,
    title: String,
    body: String,
}

#[tokio::main]
async fn main() {
    let client = ApiClient::builder()
        .base_url("https://jsonplaceholder.typicode.com")
        .build()
        .unwrap();

    // GET request — response is automatically deserialized into Post
    let response: ApiResponse<Post> = client
        .get("/posts/1", None, None)
        .await
        .unwrap();

    println!("Post #{}: {}", response.body.id, response.body.title);
}
```

## Usage Guide

### 1. Create the Client

Create one `ApiClient` and reuse it — it uses connection pooling internally.

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

let client = ApiClient::builder()
    .base_url("https://api.example.com")      // Prepended to all relative paths
    .default_timeout(Duration::from_secs(30))  // Default timeout for all requests
    .default_header("Accept", "application/json") // Sent with every request
    .session_enabled(true)                     // Auto-manage cookies & XSRF tokens
    .build()
    .unwrap();
```

### 2. GET Requests

```rust
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct User {
    name: String,
    email: String,
}

// Simple GET
let response = client.get::<Vec<User>>("/users", None, None).await?;
println!("Got {} users", response.body.len());

// GET with query parameters
let response = client
    .get::<Vec<User>>("/users", Some(&[("role", "admin"), ("limit", "10")]), None)
    .await?;
```

### 3. POST Requests

```rust
use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct CreateUser {
    name: String,
    email: String,
    password: String,
}

#[derive(Deserialize)]
struct UserResponse {
    id: String,
    name: String,
}

let new_user = CreateUser {
    name: "Keval".to_string(),
    email: "keval@example.com".to_string(),
    password: "secure123".to_string(),
};

let response = client
    .post::<UserResponse, _>("/users", Some(&new_user), None)
    .await?;

println!("Created user with ID: {}", response.body.id);
```

### 4. Per-Request Configuration

Override client defaults for individual requests:

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

let config = RequestConfig::new()
    .timeout(Duration::from_secs(5))            // Custom timeout
    .bearer_token("eyJhbGciOiJIUzI1NiJ9...")    // Authorization header
    .header("X-Request-ID", "req-12345");        // Custom header

let response = client
    .get::<serde_json::Value>("/protected/resource", None, Some(config))
    .await?;
```

### 5. Error Handling

All errors are strongly typed via the `ApiError` enum:

```rust
use rust_api_calling::ApiError;

match client.get::<serde_json::Value>("/users/999", None, None).await {
    Ok(response) => {
        println!("Status: {}", response.status);
        println!("Body: {:?}", response.body);
    }
    Err(ApiError::HttpError { status, body }) => {
        // Server returned 4xx or 5xx
        eprintln!("HTTP {}: {}", status, body);
    }
    Err(ApiError::Timeout) => {
        eprintln!("Request timed out!");
    }
    Err(ApiError::NetworkError(e)) => {
        eprintln!("Network error: {}", e);
    }
    Err(ApiError::InvalidUrl(url)) => {
        eprintln!("Bad URL: {}", url);
    }
    Err(e) => {
        eprintln!("Other error: {}", e);
    }
}
```

### 6. Using Raw JSON Responses

If you don't want to define a struct, use `serde_json::Value`:

```rust
let response = client
    .get::<serde_json::Value>("/users", None, None)
    .await?;

// Access fields dynamically
if let Some(users) = response.body.as_array() {
    for user in users {
        println!("Name: {}", user["name"]);
    }
}

// Raw body string is also available
println!("Raw JSON: {}", response.raw_body);
```

### 7. Absolute URLs (Bypass Base URL)

You can always pass a full URL to bypass the configured `base_url`:

```rust
// This ignores the base_url and calls the full URL directly
let response = client
    .get::<serde_json::Value>("https://other-api.com/data", None, None)
    .await?;
```

### 8. Session Management

When `session_enabled(true)` is set, the client automatically:
- Reads XSRF tokens from response headers (`x-xsrf-token`, `xsrf-token`, `x-csrf-token`)
- Reads cookies from `set-cookie` headers
- Attaches them to all subsequent requests

You can also manage sessions manually:

```rust
// Set session data manually
client.session.set_xsrf_token("my-token");
client.session.set_cookie("session=abc123");

// Read current session data
if let Some(token) = client.session.xsrf_token() {
    println!("Current XSRF token: {}", token);
}

// Clear all session data
client.session.clear();
```

### 9. The Core Function

All methods delegate to `make_request`. You can call it directly for full control:

```rust
use rust_api_calling::HttpMethod;

let response = client
    .make_request::<serde_json::Value, _>(
        HttpMethod::Post,
        "/endpoint",
        Some(&my_body),       // Request body (serialized to JSON)
        Some(&[("key", "val")]), // Query parameters
        Some(config),         // Per-request config
    )
    .await?;
```

### 10. Response Object

Every response includes:

```rust
let response = client.get::<MyType>("/endpoint", None, None).await?;

response.status;     // HTTP status code (u16)
response.headers;    // HashMap<String, String>
response.body;       // Deserialized body (MyType)
response.raw_body;   // Raw response string

response.is_success();            // true if 2xx
response.header("content-type");  // Case-insensitive header lookup
```

## API Reference

### `ApiClient`

| Method | Description |
|--------|-------------|
| `ApiClient::builder()` | Start building a new client |
| `.get(url, query, config)` | Perform a GET request |
| `.post(url, body, config)` | Perform a POST request |
| `.make_request(method, url, body, query, config)` | The core function — full control |

### `ApiClientBuilder`

| Method | Description |
|--------|-------------|
| `.base_url(url)` | Set the base URL for all relative paths |
| `.default_timeout(duration)` | Set default timeout (default: 60s) |
| `.default_header(key, value)` | Add a header sent with every request |
| `.session_enabled(bool)` | Enable automatic session/cookie management |
| `.build()` | Build the `ApiClient` |

### `RequestConfig`

| Method | Description |
|--------|-------------|
| `RequestConfig::new()` | Create empty per-request config |
| `.timeout(duration)` | Override timeout for this request |
| `.bearer_token(token)` | Set Authorization bearer token |
| `.header(key, value)` | Add a custom header |

### `ApiError`

| Variant | When |
|---------|------|
| `InvalidUrl(String)` | URL could not be parsed |
| `NetworkError(reqwest::Error)` | Connection, DNS, or TLS failure |
| `SerializationError(serde_json::Error)` | JSON serialize/deserialize failure |
| `HttpError { status, body }` | Server returned non-2xx status |
| `EmptyResponse` | Server returned empty body |
| `Timeout` | Request timed out |

## Logging

This library uses the [tracing](https://crates.io/crates/tracing) crate for structured logging. To see log output, add a subscriber in your application:

```rust
// Add to dev-dependencies: tracing-subscriber = { version = "0.3", features = ["env-filter"] }

tracing_subscriber::fmt()
    .with_env_filter("rust_api_calling=debug")
    .init();
```

Log levels used:
- `INFO` — Request method and URL
- `DEBUG` — Query params, request body, response body
- `WARN` — Empty responses
- `ERROR` — Network errors, timeouts, HTTP errors, deserialization failures

## Example

A full working example is included in the `examples/` directory:

```bash
cargo run --example usage
```

## License

Licensed under the [MIT License](LICENSE).