lupa 0.1.1

Interactive object inspector for Rust — web UI + TUI + snapshot diffing
Documentation
//! # Axum integration example for `lupa`
//!
//! This example demonstrates how to use `lupa` inside an async web application
//! built with [Axum](https://github.com/tokio-rs/axum). It shows that `inspect!`,
//! `snapshot!` and `snapshot_diff!` work seamlessly from any async context
//! because the global inspector state is thread‑safe and lock‑based.
//!
//! ## How to run
//! ```bash
//! cargo run --example axum_example --features="web"        # only web UI
//! cargo run --example axum_example --features="web,tui"    # web UI + TUI
//! ```
//!
//! Then:
//! - Open [http://localhost:7777](http://localhost:7777) – the lupa inspector.
//! - Send HTTP requests to the Axum server (port 3000):
//!   ```bash
//!   curl "http://localhost:3000/update?age=35"
//!   curl "http://localhost:3000/promote"
//!   curl "http://localhost:3000/activity?points=10"
//!   curl "http://localhost:3000/state"
//!   ```
//! - Watch the inspector update in real time (snapshots and diffs appear
//!   instantly thanks to WebSocket push).
//!
//! ## What this example shows
//! - Shared state (`Arc<Mutex<User>>`) that is mutated inside async handlers.
//! - `inspect!` capturing the state after each mutation.
//! - `snapshot!` capturing a "before" state and `snapshot_diff!` showing
//!   exactly what changed.
//! - The inspector server runs in the background while Axum handles requests.

use axum::{
    extract::{Query, State},
    response::Json,
    routing::get,
    Router,
};
use lupa::{inspect, snapshot, snapshot_diff, RunMode};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;

// ---------- Application state (the data we want to inspect) ----------

/// A user entity with various fields to demonstrate snapshot diffing.
#[derive(Debug, Clone)]
struct User {
    id: u64,
    name: String,
    email: String,
    age: u32,
    role: Role,
    address: Address,
    tags: Vec<String>,
    scores: HashMap<String, f32>,
    active: bool,
}

#[derive(Debug, Clone)]
enum Role {
    Guest,
    Moderator,
    Admin { since: String },
}

#[derive(Debug, Clone)]
struct Address {
    city: String,
    country: String,
    zip: String,
}

impl User {
    /// Creates a new user with default values.
    fn new(id: u64, name: &str, email: &str) -> Self {
        Self {
            id,
            name: name.into(),
            email: email.into(),
            age: 28,
            role: Role::Guest,
            address: Address {
                city: "Berlin".into(),
                country: "DE".into(),
                zip: "10115".into(),
            },
            tags: vec!["newcomer".into()],
            scores: HashMap::from([
                ("reputation".into(), 10.0),
                ("activity".into(), 0.0),
            ]),
            active: true,
        }
    }

    /// Promotes the user to admin, changing role, tags and reputation.
    fn promote_to_admin(&mut self) {
        self.role = Role::Admin {
            since: "2024-06-01".into(),
        };
        self.tags.push("admin".into());
        self.tags.retain(|t| t != "newcomer");
        *self.scores.entry("reputation".into()).or_default() += 500.0;
        self.active = true;
    }

    /// Records user activity: increases activity score and increments age.
    fn record_activity(&mut self, points: f32) {
        *self.scores.entry("activity".into()).or_default() += points;
        self.age += 1;
    }

    /// Updates the user's age.
    fn set_age(&mut self, age: u32) {
        self.age = age;
    }
}

/// Shared state type – an `Arc<Mutex<User>>` allows safe concurrent access.
type AppState = Arc<Mutex<User>>;

// ---------- Axum request handlers ----------

/// Handler for `GET /update?age=N`. Updates the user's age.
async fn handle_update_age(
    State(state): State<AppState>,
    Query(params): Query<HashMap<String, String>>,
) -> Json<serde_json::Value> {
    let age = params.get("age").and_then(|v| v.parse::<u32>().ok());
    if let Some(age) = age {
        let mut user = state.lock().unwrap();
        user.set_age(age);
        // 👇 Snapshot is sent to lupa immediately.
        inspect!(user);
        Json(serde_json::json!({ "status": "ok", "new_age": age }))
    } else {
        Json(serde_json::json!({ "error": "missing or invalid 'age' parameter" }))
    }
}

/// Handler for `GET /promote`. Promotes the user to admin and shows a diff.
async fn handle_promote(State(state): State<AppState>) -> Json<serde_json::Value> {
    let mut user = state.lock().unwrap();
    // Capture the state *before* promotion.
    let before = snapshot!(user);
    user.promote_to_admin();
    // Another snapshot after the change.
    inspect!(user);
    // Compute and send a diff – appears in the "Diffs" tab.
    snapshot_diff!(before, user);
    Json(serde_json::json!({ "status": "promoted to admin" }))
}

/// Handler for `GET /activity?points=X`. Records activity points.
async fn handle_activity(
    State(state): State<AppState>,
    Query(params): Query<HashMap<String, String>>,
) -> Json<serde_json::Value> {
    let points = params
        .get("points")
        .and_then(|v| v.parse::<f32>().ok())
        .unwrap_or(10.0);
    let mut user = state.lock().unwrap();
    user.record_activity(points);
    inspect!(user);
    Json(serde_json::json!({ "status": "activity recorded", "points": points }))
}

/// Handler for `GET /state`. Returns a simplified view of the current user.
async fn handle_state(State(state): State<AppState>) -> Json<serde_json::Value> {
    let user = state.lock().unwrap();
    Json(serde_json::json!({
        "id": user.id,
        "name": user.name,
        "email": user.email,
        "age": user.age,
        "active": user.active,
    }))
}

// ---------- Main: start both Axum and lupa ----------

#[tokio::main]
async fn main() {
    // 1. Create the shared state.
    let user = User::new(42, "Alice", "alice@example.com");
    let state = Arc::new(Mutex::new(user));

    // 2. Build the Axum router.
    let app = Router::new()
        .route("/update", get(handle_update_age))
        .route("/promote", get(handle_promote))
        .route("/activity", get(handle_activity))
        .route("/state", get(handle_state))
        .with_state(state.clone());

    // 3. Start the Axum server on port 3000.
    let axum_listener = TcpListener::bind("0.0.0.0:3000")
        .await
        .expect("Failed to bind Axum server");
    println!("🌐 Axum server running on http://localhost:3000");

    // Spawn Axum in a background task so lupa can run in the main thread.
    tokio::spawn(async move {
        axum::serve(axum_listener, app)
            .await
            .expect("Axum server error");
    });

    // 4. Take an initial snapshot so the inspector shows the starting state.
    {
        let user_guard = state.lock().unwrap();
        inspect!(user_guard);
    }

    // 5. Start the lupa inspector (web UI only in this example).
    println!("🔍 Lupa inspector available at http://localhost:7777");
    // Run the web server and block until Ctrl+C.
    lupa::run_mode(RunMode::Web).unwrap();
}