resuma 1.2.0

Resuma — resumable SSR Rust web framework: zero hydration, islands, server actions, Flow (Axum).
Documentation
//! Global registry for `#[server]` actions.

use std::collections::{BTreeMap, HashMap};
use std::future::Future;
use std::pin::Pin;

use crate::core::{FlowRequest, Result, ResumaError};
use once_cell::sync::Lazy;
use parking_lot::RwLock;
use serde_json::Value;

pub type ActionFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send>>;
pub type ActionFn = fn(Vec<Value>, FlowRequest) -> ActionFuture;

pub type ActionMiddlewareFuture = Pin<Box<dyn Future<Output = Result<FlowRequest>> + Send>>;
pub type ActionMiddlewareFn = fn(FlowRequest) -> ActionMiddlewareFuture;

static REGISTRY: Lazy<RwLock<HashMap<String, ActionFn>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));
static ACTION_MIDDLEWARE: Lazy<RwLock<Option<ActionMiddlewareFn>>> =
    Lazy::new(|| RwLock::new(None));

/// Register a global middleware pipeline for `/_resuma/action/*` requests.
pub fn set_action_middleware(f: ActionMiddlewareFn) {
    *ACTION_MIDDLEWARE.write() = Some(f);
}

pub fn register_server_action(name: &str, f: ActionFn) {
    let mut reg = REGISTRY.write();
    if reg.contains_key(name) {
        panic!("server action `{name}` registered twice");
    }
    reg.insert(name.to_string(), f);
}

pub fn get_action(name: &str) -> Option<ActionFn> {
    REGISTRY.read().get(name).copied()
}

pub async fn dispatch(name: &str, args: Vec<Value>, mut req: FlowRequest) -> Result<Value> {
    let middleware = *ACTION_MIDDLEWARE.read();
    if let Some(mw) = middleware {
        req = mw(req).await?;
    }
    match get_action(name) {
        Some(f) => f(args, req).await,
        None => Err(ResumaError::UnknownAction(name.to_string())),
    }
}

/// Parse query string into a map (URL-decoded).
pub fn parse_query(query: &str) -> BTreeMap<String, String> {
    crate::flow::request::parse_query(if query.is_empty() {
        None
    } else {
        Some(query)
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_query_decodes_percent_encoding() {
        let q = parse_query("msg=hello%20world");
        assert_eq!(q.get("msg").map(String::as_str), Some("hello world"));
    }
}