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;
#[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 {
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,
}
}
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;
}
fn record_activity(&mut self, points: f32) {
*self.scores.entry("activity".into()).or_default() += points;
self.age += 1;
}
fn set_age(&mut self, age: u32) {
self.age = age;
}
}
type AppState = Arc<Mutex<User>>;
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);
inspect!(user);
Json(serde_json::json!({ "status": "ok", "new_age": age }))
} else {
Json(serde_json::json!({ "error": "missing or invalid 'age' parameter" }))
}
}
async fn handle_promote(State(state): State<AppState>) -> Json<serde_json::Value> {
let mut user = state.lock().unwrap();
let before = snapshot!(user);
user.promote_to_admin();
inspect!(user);
snapshot_diff!(before, user);
Json(serde_json::json!({ "status": "promoted to admin" }))
}
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 }))
}
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,
}))
}
#[tokio::main]
async fn main() {
let user = User::new(42, "Alice", "alice@example.com");
let state = Arc::new(Mutex::new(user));
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());
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");
tokio::spawn(async move {
axum::serve(axum_listener, app)
.await
.expect("Axum server error");
});
{
let user_guard = state.lock().unwrap();
inspect!(user_guard);
}
println!("🔍 Lupa inspector available at http://localhost:7777");
lupa::run_mode(RunMode::Web).unwrap();
}