hyperlite 0.1.0

Lightweight HTTP framework built on hyper, tokio, and tower
Documentation
//! Stateful Hyperlite server demonstrating shared application context and
//! request extractors.
//!
//! Run with:
//! ```bash
//! cargo run --example with_state
//! ```
//!
//! Helpful curl commands:
//! ```bash
//! curl http://127.0.0.1:3000/stats
//!
//! curl -X POST http://127.0.0.1:3000/users \
//!   -H "Content-Type: application/json" \
//!   -d '{"name":"Alice","email":"alice@example.com"}'
//!
//! curl "http://127.0.0.1:3000/users?limit=2&offset=0"
//!
//! curl http://127.0.0.1:3000/users/6f9619ff-8b86-d011-b42d-00cf4fc964ff
//! ```

use bytes::Bytes;
use http_body_util::Full;
use hyper::{Method, Request, Response, StatusCode};
use hyperlite::{
    parse_json_body, path_param, query_params, serve, success, BoxBody, BoxError, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

/// Application-wide state shared across every handler invocation.
#[derive(Clone)]
struct AppState {
    counter: Arc<Mutex<u64>>,
    app_name: String,
}

impl AppState {
    fn new(name: impl Into<String>) -> Self {
        Self {
            counter: Arc::new(Mutex::new(0)),
            app_name: name.into(),
        }
    }
}

/// In-memory representation of a user resource.
#[derive(Clone, Serialize)]
struct User {
    id: Uuid,
    name: String,
    email: String,
}

/// Request payload for creating a new user.
#[derive(Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

/// Query parameters accepted by the list users endpoint.
#[derive(Default, Deserialize)]
struct ListUsersQuery {
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

/// Summary statistics returned from `/stats`.
#[derive(Serialize)]
struct StatsResponse {
    app: String,
    total_users_created: u64,
}

async fn get_stats_handler(
    _req: Request<BoxBody>,
    state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, BoxError> {
    let total_users = *state.counter.lock().expect("counter poisoned");

    let payload = StatsResponse {
        app: state.app_name.clone(),
        total_users_created: total_users,
    };

    Ok(success(StatusCode::OK, payload))
}

async fn create_user_handler(
    req: Request<BoxBody>,
    state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, BoxError> {
    let body = parse_json_body::<CreateUserRequest>(req).await?;
    let mut counter = state.counter.lock().expect("counter poisoned");
    *counter += 1;

    let user = User {
        id: Uuid::new_v4(),
        name: body.name,
        email: body.email,
    };

    Ok(success(StatusCode::CREATED, user))
}

async fn list_users_handler(
    req: Request<BoxBody>,
    _state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, BoxError> {
    let params = query_params::<ListUsersQuery>(&req)?;

    let mut users = vec![
        User {
            id: Uuid::new_v4(),
            name: "Alice".into(),
            email: "alice@example.com".into(),
        },
        User {
            id: Uuid::new_v4(),
            name: "Bob".into(),
            email: "bob@example.com".into(),
        },
        User {
            id: Uuid::new_v4(),
            name: "Charlie".into(),
            email: "charlie@example.com".into(),
        },
    ];

    if let Some(offset) = params.offset {
        let skip = offset.min(users.len() as u32) as usize;
        users = users.into_iter().skip(skip).collect();
    }

    if let Some(limit) = params.limit {
        let take = limit.min(users.len() as u32) as usize;
        users.truncate(take);
    }

    Ok(success(StatusCode::OK, users))
}

async fn get_user_handler(
    req: Request<BoxBody>,
    _state: Arc<AppState>,
) -> Result<Response<Full<Bytes>>, BoxError> {
    let user_id: Uuid = path_param(&req, "id")?;

    let user = User {
        id: user_id,
        name: "Requested User".into(),
        email: "requested@example.com".into(),
    };

    Ok(success(StatusCode::OK, user))
}

#[tokio::main]
async fn main() -> Result<(), BoxError> {
    let state = AppState::new("Hyperlite Demo");
    let router = Router::new(state.clone())
        .route(
            "/stats",
            Method::GET,
            Arc::new(|req, state| Box::pin(get_stats_handler(req, state))),
        )
        .route(
            "/users",
            Method::POST,
            Arc::new(|req, state| Box::pin(create_user_handler(req, state))),
        )
        .route(
            "/users",
            Method::GET,
            Arc::new(|req, state| Box::pin(list_users_handler(req, state))),
        )
        .route(
            "/users/{id}",
            Method::GET,
            Arc::new(|req, state| Box::pin(get_user_handler(req, state))),
        );

    let addr: SocketAddr = "127.0.0.1:3000"
        .parse()
        .expect("valid socket address for example server");
    println!(
        "Stateful example available at http://{addr}\n  • GET /stats\n  • POST /users\n  • GET /users\n  • GET /users/:id"
    );

    serve(addr, router).await
}