what-core 1.7.0

Core framework for What - an HTML-first web framework powered by Rust
Documentation
//! Action handlers for w-* attributes
//!
//! Processes form actions and AJAX-like interactions without JavaScript.

use std::collections::HashMap;

/// Action types supported by the framework
#[derive(Debug, Clone, PartialEq)]
pub enum ActionType {
    /// Create a new item in a collection
    Create,
    /// Update an existing item
    Update,
    /// Delete an item
    Delete,
    /// Navigate to another page
    Navigate,
    /// Refresh/reload current content
    Refresh,
    /// Toggle a UI element
    Toggle,
    /// Custom action
    Custom(String),
}

impl From<&str> for ActionType {
    fn from(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "create" | "new" | "add" => Self::Create,
            "update" | "edit" | "save" => Self::Update,
            "delete" | "remove" | "destroy" => Self::Delete,
            "navigate" | "goto" | "redirect" => Self::Navigate,
            "refresh" | "reload" => Self::Refresh,
            "toggle" => Self::Toggle,
            other => Self::Custom(other.to_string()),
        }
    }
}

/// Parsed action from w-* attributes
#[derive(Debug, Clone)]
pub struct ParsedAction {
    /// The action type
    pub action_type: ActionType,
    /// Target collection (for CRUD)
    pub collection: Option<String>,
    /// Target ID (for update/delete)
    pub id: Option<String>,
    /// Redirect URL after action
    pub redirect: Option<String>,
    /// Target element for partial updates
    pub target: Option<String>,
    /// Swap mode for partial updates
    pub swap: SwapMode,
    /// Confirmation message (if any)
    pub confirm: Option<String>,
}

/// How to swap content in partial updates
#[derive(Debug, Clone, Default)]
pub enum SwapMode {
    /// Replace inner HTML
    #[default]
    InnerHtml,
    /// Replace outer HTML (including the element)
    OuterHtml,
    /// Insert before the element
    BeforeBegin,
    /// Insert after the element
    AfterEnd,
    /// Append to the element
    BeforeEnd,
    /// Prepend to the element
    AfterBegin,
    /// Don't swap, just trigger action
    None,
}

impl From<&str> for SwapMode {
    fn from(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "innerhtml" | "inner" => Self::InnerHtml,
            "outerhtml" | "outer" => Self::OuterHtml,
            "beforebegin" | "before" => Self::BeforeBegin,
            "afterend" | "after" => Self::AfterEnd,
            "beforeend" | "append" => Self::BeforeEnd,
            "afterbegin" | "prepend" => Self::AfterBegin,
            "none" | "false" => Self::None,
            _ => Self::InnerHtml,
        }
    }
}

/// Handler for processing actions
pub struct ActionHandler;

impl ActionHandler {
    /// Parse w-* attributes from a form or element
    pub fn parse_attributes(attrs: &HashMap<String, String>) -> ParsedAction {
        let action_str = attrs
            .get("w-action")
            .map(|s| s.as_str())
            .unwrap_or("create");
        let action_type = ActionType::from(action_str);

        ParsedAction {
            action_type,
            collection: attrs
                .get("w-store")
                .cloned()
                .or_else(|| attrs.get("w-collection").cloned()),
            id: attrs.get("w-id").cloned(),
            redirect: attrs.get("w-redirect").cloned(),
            target: attrs.get("w-target").cloned(),
            swap: attrs
                .get("w-swap")
                .map(|s| SwapMode::from(s.as_str()))
                .unwrap_or_default(),
            confirm: attrs.get("w-confirm").cloned(),
        }
    }

    /// Generate the action URL for a form
    pub fn generate_action_url(action: &ParsedAction) -> String {
        let mut url = String::from("/w-action");

        if let Some(ref collection) = action.collection {
            url.push('/');
            url.push_str(collection);

            if let Some(ref id) = action.id {
                url.push('/');
                url.push_str(id);
            }
        }

        // Add query params
        let mut params = Vec::new();

        match action.action_type {
            ActionType::Create => params.push("w-action=create".to_string()),
            ActionType::Update => params.push("w-action=update".to_string()),
            ActionType::Delete => params.push("w-action=delete".to_string()),
            ActionType::Custom(ref name) => params.push(format!("w-action={}", name)),
            _ => {}
        }

        if let Some(ref redirect) = action.redirect {
            params.push(format!("w-redirect={}", urlencoding::encode(redirect)));
        }

        if !params.is_empty() {
            url.push('?');
            url.push_str(&params.join("&"));
        }

        url
    }
}

// URL encoding uses the `urlencoding` crate from workspace dependencies

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

    #[test]
    fn test_parse_action_type() {
        assert_eq!(ActionType::from("create"), ActionType::Create);
        assert_eq!(ActionType::from("update"), ActionType::Update);
        assert_eq!(ActionType::from("delete"), ActionType::Delete);
        assert_eq!(
            ActionType::from("custom_action"),
            ActionType::Custom("custom_action".to_string())
        );
    }

    #[test]
    fn test_parse_attributes() {
        let mut attrs = HashMap::new();
        attrs.insert("w-action".to_string(), "create".to_string());
        attrs.insert("w-store".to_string(), "posts".to_string());
        attrs.insert("w-redirect".to_string(), "/posts".to_string());

        let action = ActionHandler::parse_attributes(&attrs);

        assert_eq!(action.action_type, ActionType::Create);
        assert_eq!(action.collection, Some("posts".to_string()));
        assert_eq!(action.redirect, Some("/posts".to_string()));
    }

    #[test]
    fn test_generate_action_url() {
        let action = ParsedAction {
            action_type: ActionType::Create,
            collection: Some("posts".to_string()),
            id: None,
            redirect: Some("/posts".to_string()),
            target: None,
            swap: SwapMode::InnerHtml,
            confirm: None,
        };

        let url = ActionHandler::generate_action_url(&action);
        assert!(url.starts_with("/w-action/posts"));
        assert!(url.contains("w-action=create"));
    }
}