AkurAI Build
Menu

Public repository / 69a2606c7ff3e89d2f50090fa59a15f14171a37a

AkurAI-Framework / crates/cli/src/guestbook.rs

Latest change 69a2606c7ff3e89d2f50090fa59a15f14171a37a - feat: HTTP request bodies + form actions (guestbook demo, progressive enhancement) by Ólafur Búi Ólafsson

//! A guestbook — the form-actions demo.
//!
//! `GET /guestbook` lists entries; `POST /guestbook` validates a plain HTML
//! form, stores the entry, and redirects back (Post/Redirect/Get, so a refresh
//! never resubmits). It works with JavaScript disabled — the progressive-
//! enhancement baseline the framework is built for. Entries are in-memory and
//! reset on restart; durable storage is the next slice (wire in the B+tree).
//!
//! It also stands in for the "loader" pattern: the handler builds a context
//! (the entry list) for the template, by hand for now — exactly the boilerplate
//! a declarative per-route `load` will later remove.

use crate::cmd_serve::{build_engine, error_page, load_context};
use akurai_http::{form, Method, Reply, Request, Response};
use akurai_json::Value;
use std::path::Path;
use std::sync::{Arc, Mutex};

/// Shared, in-memory guestbook entries. Cheap to clone (an `Arc`), so the
/// request handler can hold one copy for the server's life.
#[derive(Clone, Default)]
pub struct State {
    entries: Arc<Mutex<Vec<Entry>>>,
}

#[derive(Clone)]
struct Entry {
    name: String,
    message: String,
}

impl State {
    pub fn new() -> State {
        State::default()
    }
}

/// Route `GET`/`POST /guestbook`.
pub fn handle(state: &State, root: &Path, req: &Request) -> Reply {
    let response = match req.method {
        Method::Post => submit(state, root, req),
        _ => render_form(state, root, "", "", ""),
    };
    Reply::Response(response)
}

/// Handle a form submission: validate, store, redirect.
fn submit(state: &State, root: &Path, req: &Request) -> Response {
    let pairs = form::parse_urlencoded(&req.body_str());
    let name = form::field(&pairs, "name").unwrap_or("").trim().to_string();
    let message = form::field(&pairs, "message")
        .unwrap_or("")
        .trim()
        .to_string();

    if name.is_empty() || message.is_empty() {
        // Re-render with the error and the values the user already typed.
        return render_form(
            state,
            root,
            "Both a name and a message are required.",
            &name,
            &message,
        );
    }

    state.entries.lock().unwrap().push(Entry { name, message });
    // Post/Redirect/Get: send the browser back to the listing with a GET.
    Response::new(303).with_header("Location", "/guestbook")
}

/// Render the guestbook template with the entry list and optional form state.
fn render_form(state: &State, root: &Path, error: &str, name: &str, message: &str) -> Response {
    let engine = match build_engine(root) {
        Ok(engine) => engine,
        Err(msg) => return Response::new(500).with_html(&error_page(&msg)),
    };

    // Snapshot entries newest-first for display, releasing the lock promptly.
    let (entries, count) = {
        let guard = state.entries.lock().unwrap();
        let list = guard
            .iter()
            .rev()
            .map(|e| {
                Value::Object(vec![
                    ("name".into(), Value::Str(e.name.clone())),
                    ("message".into(), Value::Str(e.message.clone())),
                ])
            })
            .collect();
        (Value::Array(list), guard.len() as i64)
    };

    let mut context = load_context(root);
    if let Value::Object(pairs) = &mut context {
        pairs.push(("entries".into(), entries));
        pairs.push(("count".into(), Value::Int(count)));
        pairs.push(("error".into(), Value::Str(error.to_string())));
        pairs.push(("form_name".into(), Value::Str(name.to_string())));
        pairs.push(("form_message".into(), Value::Str(message.to_string())));
    }

    match engine.render("guestbook", &context) {
        Ok(html) => Response::ok().with_html(&html),
        Err(e) => Response::new(500).with_html(&error_page(&e.message)),
    }
}