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
//! # Usage Example — rust_api_calling
//!
//! Demonstrates all key features by calling the deployed CRUD API at:
//!   https://rust-crud-pay9.onrender.com
//!
//! Endpoints used:
//!   GET  /                → Health check
//!   GET  /get-all-users   → List all users
//!   GET  /get-user/{id}   → Get a single user by ID
//!   POST /add-user        → Create a new user
//!
//! Run with: `cargo run --example usage`

use rust_api_calling::{ApiClient, ApiError, RequestConfig};
use serde::{Deserialize, Serialize};
use std::time::Duration;

// ─── Data Models (matching your server's User struct) ───────────────────────

/// The User model — matches your server's `User` struct exactly.
/// All fields are `Option<String>` just like on the server side.
#[derive(Debug, Serialize, Deserialize)]
struct User {
    #[serde(rename = "_id", skip_serializing)]
    id: Option<serde_json::Value>, // MongoDB ObjectId comes as { "$oid": "..." }
    name: Option<String>,
    email: Option<String>,
    password: Option<String>,
    role: Option<String>,
    number: Option<String>,
}

/// Request body for creating a new user via POST /add-user.
/// We don't include `_id` because MongoDB generates it.
#[derive(Debug, Serialize)]
struct CreateUserRequest {
    name: String,
    email: String,
    password: String,
    role: String,
    number: String,
}

// ─── Main ───────────────────────────────────────────────────────────────────

#[tokio::main]
async fn main() {
    // Initialize tracing for structured log output.
    // Use `RUST_LOG=debug cargo run --example usage` for verbose output.
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info".into()),
        )
        .init();

    println!("╔══════════════════════════════════════════════════════════════╗");
    println!("║       rust_api_calling — CRUD API Usage Examples            ║");
    println!("║       Server: https://rust-crud-pay9.onrender.com          ║");
    println!("╚══════════════════════════════════════════════════════════════╝\n");

    // ── 1. Create the client ONCE — reuse it for every call ─────────────
    //
    //    This is the Rust equivalent of your Swift `WebService` singleton.
    //    The base_url means you only write relative paths like "/get-all-users".
    let client = ApiClient::builder()
        .base_url("https://rust-crud-pay9.onrender.com")
        .default_timeout(Duration::from_secs(60)) // Render free tier can be slow to wake
        .default_header("Accept", "application/json")
        .session_enabled(true) // Auto-manage cookies & XSRF tokens
        .build()
        .expect("Failed to create ApiClient");

    println!("✅ ApiClient created with base_url and session management\n");

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 1: Health Check — GET /
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 1: GET / (Health Check) ━━━");
    println!("    Waking up the server (Render free tier may take ~30s)...\n");

    match client.get::<String>("/", None, None).await {
        Ok(response) => {
            println!("  ✅ Status:   {}", response.status);
            println!("  📝 Response: {}", response.body);
        }
        Err(e) => println!("  ❌ Error: {}\n", e),
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 2: Create a New User — POST /add-user
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 2: POST /add-user (Create User) ━━━");

    let new_user = CreateUserRequest {
        name: "Keval Thumar".to_string(),
        email: "kevalthumar@gmail.com".to_string(),
        password: "fsdfuphnsd921kjds".to_string(),
        role: "developer".to_string(),
        number: "9876543210".to_string(),
    };

    println!("  📤 Sending: {:?}\n", new_user);

    match client
        .post::<serde_json::Value, _>("/add-user", Some(&new_user), None)
        .await
    {
        Ok(response) => {
            println!("  ✅ Status:   {}", response.status);
            println!(
                "  📝 Response: {}",
                serde_json::to_string_pretty(&response.body).unwrap()
            );
        }
        Err(ApiError::HttpError { status, body }) => {
            println!("  ⚠️  HTTP {}: {}", status, body);
        }
        Err(e) => println!("  ❌ Error: {}", e),
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 3: Get All Users — GET /get-all-users
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 3: GET /get-all-users (List All) ━━━");

    match client
        .get::<serde_json::Value>("/get-all-users", None, None)
        .await
    {
        Ok(response) => {
            println!("  ✅ Status: {}", response.status);

            // If the response is an array, show count + first few users
            if let Some(users) = response.body.as_array() {
                println!("  👥 Total users: {}", users.len());
                for (i, user) in users.iter().take(3).enumerate() {
                    println!(
                        "     [{}] name: {}, email: {}, role: {}",
                        i + 1,
                        user.get("name").and_then(|v| v.as_str()).unwrap_or("N/A"),
                        user.get("email").and_then(|v| v.as_str()).unwrap_or("N/A"),
                        user.get("role").and_then(|v| v.as_str()).unwrap_or("N/A"),
                    );
                }
                if users.len() > 3 {
                    println!("     ... and {} more", users.len() - 3);
                }
            } else {
                println!(
                    "  📝 Response: {}",
                    serde_json::to_string_pretty(&response.body).unwrap()
                );
            }
        }
        Err(e) => println!("  ❌ Error: {}", e),
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 4: Get All Users — deserialized into Vec<User>
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 4: GET /get-all-users (Typed into Vec<User>) ━━━");

    match client
        .get::<Vec<User>>("/get-all-users", None, None)
        .await
    {
        Ok(response) => {
            println!("  ✅ Status: {}", response.status);
            println!("  👥 Deserialized {} users into Vec<User>", response.body.len());
            for (i, user) in response.body.iter().take(3).enumerate() {
                println!(
                    "     [{}] {:?}{} ({})",
                    i + 1,
                    user.name.as_deref().unwrap_or("N/A"),
                    user.email.as_deref().unwrap_or("N/A"),
                    user.role.as_deref().unwrap_or("N/A"),
                );
            }
        }
        Err(e) => println!("  ❌ Error: {}", e),
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 5: Get a Specific User — GET /get-user/{id}
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 5: GET /get-user/{{id}} (Single User) ━━━");
    println!("    First, fetching all users to get a valid ID...\n");

    // Grab the first user's ID from the list, then fetch that specific user.
    let first_user_id = client
        .get::<serde_json::Value>("/get-all-users", None, None)
        .await
        .ok()
        .and_then(|resp| {
            resp.body
                .as_array()?
                .first()?
                .get("_id")?
                .get("$oid")
                .and_then(|v| v.as_str().map(String::from))
        });

    if let Some(id) = first_user_id {
        println!("  🔍 Fetching user with ID: {}", id);

        match client
            .get::<serde_json::Value>(&format!("/get-user/{}", id), None, None)
            .await
        {
            Ok(response) => {
                println!("  ✅ Status: {}", response.status);
                println!(
                    "  📝 User: {}",
                    serde_json::to_string_pretty(&response.body).unwrap()
                );
            }
            Err(e) => println!("  ❌ Error: {}", e),
        }
    } else {
        println!("  ⚠️  No users found — skipping single user fetch");
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 6: Error Handling — GET /get-user/{invalid_id}
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 6: Error Handling (invalid user ID) ━━━");

    match client
        .get::<serde_json::Value>("/get-user/69c8cf69d80a457f574df9b2", None, None)
        .await
    {
        Ok(response) => {
            println!("  📝 Status: {} — Response: {}", response.status, response.raw_body);
        }
        Err(ApiError::HttpError { status, body }) => {
            println!("  ✅ Got expected HTTP error!");
            println!("  ⚠️  Status: {}", status);
            println!("  📝 Body:   {}", body);
        }
        Err(e) => {
            println!("  ❌ Error: {}", e);
        }
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 7: Custom Per-Request Config
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 7: Custom RequestConfig (timeout + headers) ━━━");

    let custom_config = RequestConfig::new()
        .timeout(Duration::from_secs(10))
        .header("X-Request-ID", "example-rust-12345")
        .header("X-Client-Version", "1.0.0");

    match client
        .get::<serde_json::Value>("/get-all-users", None, Some(custom_config))
        .await
    {
        Ok(response) => {
            println!("  ✅ Status: {}", response.status);
            println!("  📋 Response headers received: {} total", response.headers.len());
            if let Some(ct) = response.header("content-type") {
                println!("     Content-Type: {}", ct);
            }
        }
        Err(e) => println!("  ❌ Error: {}", e),
    }
    println!();

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // Example 8: Using an Absolute URL (bypasses base_url)
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    println!("━━━ Example 8: Absolute URL (external API, bypasses base_url) ━━━");

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "camelCase")]
    #[allow(dead_code)]
    struct Post {
        id: u32,
        title: String,
        body: String,
        user_id: u32,
    }

    match client
        .get::<Post>("https://jsonplaceholder.typicode.com/posts/1", None, None)
        .await
    {
        Ok(response) => {
            let post = &response.body;
            println!("  ✅ Status: {}", response.status);
            println!("  📝 Post #{}: {}", post.id, post.title);
            println!("     Body: {}...", &post.body[..60.min(post.body.len())]);
        }
        Err(e) => println!("  ❌ Error: {}", e),
    }
    println!();

    println!("╔══════════════════════════════════════════════════════════════╗");
    println!("║       All 8 examples completed!                            ║");
    println!("╚══════════════════════════════════════════════════════════════╝");
}