Skip to main content

what_core/server/
actions.rs

1//! Action handlers for w-* attributes
2//!
3//! Processes form actions and AJAX-like interactions without JavaScript.
4
5use std::collections::HashMap;
6
7/// Action types supported by the framework
8#[derive(Debug, Clone, PartialEq)]
9pub enum ActionType {
10    /// Create a new item in a collection
11    Create,
12    /// Update an existing item
13    Update,
14    /// Delete an item
15    Delete,
16    /// Navigate to another page
17    Navigate,
18    /// Refresh/reload current content
19    Refresh,
20    /// Toggle a UI element
21    Toggle,
22    /// Custom action
23    Custom(String),
24}
25
26impl From<&str> for ActionType {
27    fn from(s: &str) -> Self {
28        match s.to_lowercase().as_str() {
29            "create" | "new" | "add" => Self::Create,
30            "update" | "edit" | "save" => Self::Update,
31            "delete" | "remove" | "destroy" => Self::Delete,
32            "navigate" | "goto" | "redirect" => Self::Navigate,
33            "refresh" | "reload" => Self::Refresh,
34            "toggle" => Self::Toggle,
35            other => Self::Custom(other.to_string()),
36        }
37    }
38}
39
40/// Parsed action from w-* attributes
41#[derive(Debug, Clone)]
42pub struct ParsedAction {
43    /// The action type
44    pub action_type: ActionType,
45    /// Target collection (for CRUD)
46    pub collection: Option<String>,
47    /// Target ID (for update/delete)
48    pub id: Option<String>,
49    /// Redirect URL after action
50    pub redirect: Option<String>,
51    /// Target element for partial updates
52    pub target: Option<String>,
53    /// Swap mode for partial updates
54    pub swap: SwapMode,
55    /// Confirmation message (if any)
56    pub confirm: Option<String>,
57}
58
59/// How to swap content in partial updates
60#[derive(Debug, Clone, Default)]
61pub enum SwapMode {
62    /// Replace inner HTML
63    #[default]
64    InnerHtml,
65    /// Replace outer HTML (including the element)
66    OuterHtml,
67    /// Insert before the element
68    BeforeBegin,
69    /// Insert after the element
70    AfterEnd,
71    /// Append to the element
72    BeforeEnd,
73    /// Prepend to the element
74    AfterBegin,
75    /// Don't swap, just trigger action
76    None,
77}
78
79impl From<&str> for SwapMode {
80    fn from(s: &str) -> Self {
81        match s.to_lowercase().as_str() {
82            "innerhtml" | "inner" => Self::InnerHtml,
83            "outerhtml" | "outer" => Self::OuterHtml,
84            "beforebegin" | "before" => Self::BeforeBegin,
85            "afterend" | "after" => Self::AfterEnd,
86            "beforeend" | "append" => Self::BeforeEnd,
87            "afterbegin" | "prepend" => Self::AfterBegin,
88            "none" | "false" => Self::None,
89            _ => Self::InnerHtml,
90        }
91    }
92}
93
94/// Handler for processing actions
95pub struct ActionHandler;
96
97impl ActionHandler {
98    /// Parse w-* attributes from a form or element
99    pub fn parse_attributes(attrs: &HashMap<String, String>) -> ParsedAction {
100        let action_str = attrs
101            .get("w-action")
102            .map(|s| s.as_str())
103            .unwrap_or("create");
104        let action_type = ActionType::from(action_str);
105
106        ParsedAction {
107            action_type,
108            collection: attrs
109                .get("w-store")
110                .cloned()
111                .or_else(|| attrs.get("w-collection").cloned()),
112            id: attrs.get("w-id").cloned(),
113            redirect: attrs.get("w-redirect").cloned(),
114            target: attrs.get("w-target").cloned(),
115            swap: attrs
116                .get("w-swap")
117                .map(|s| SwapMode::from(s.as_str()))
118                .unwrap_or_default(),
119            confirm: attrs.get("w-confirm").cloned(),
120        }
121    }
122
123    /// Generate the action URL for a form
124    pub fn generate_action_url(action: &ParsedAction) -> String {
125        let mut url = String::from("/w-action");
126
127        if let Some(ref collection) = action.collection {
128            url.push('/');
129            url.push_str(collection);
130
131            if let Some(ref id) = action.id {
132                url.push('/');
133                url.push_str(id);
134            }
135        }
136
137        // Add query params
138        let mut params = Vec::new();
139
140        match action.action_type {
141            ActionType::Create => params.push("w-action=create".to_string()),
142            ActionType::Update => params.push("w-action=update".to_string()),
143            ActionType::Delete => params.push("w-action=delete".to_string()),
144            ActionType::Custom(ref name) => params.push(format!("w-action={}", name)),
145            _ => {}
146        }
147
148        if let Some(ref redirect) = action.redirect {
149            params.push(format!("w-redirect={}", urlencoding::encode(redirect)));
150        }
151
152        if !params.is_empty() {
153            url.push('?');
154            url.push_str(&params.join("&"));
155        }
156
157        url
158    }
159}
160
161// URL encoding uses the `urlencoding` crate from workspace dependencies
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_parse_action_type() {
169        assert_eq!(ActionType::from("create"), ActionType::Create);
170        assert_eq!(ActionType::from("update"), ActionType::Update);
171        assert_eq!(ActionType::from("delete"), ActionType::Delete);
172        assert_eq!(
173            ActionType::from("custom_action"),
174            ActionType::Custom("custom_action".to_string())
175        );
176    }
177
178    #[test]
179    fn test_parse_attributes() {
180        let mut attrs = HashMap::new();
181        attrs.insert("w-action".to_string(), "create".to_string());
182        attrs.insert("w-store".to_string(), "posts".to_string());
183        attrs.insert("w-redirect".to_string(), "/posts".to_string());
184
185        let action = ActionHandler::parse_attributes(&attrs);
186
187        assert_eq!(action.action_type, ActionType::Create);
188        assert_eq!(action.collection, Some("posts".to_string()));
189        assert_eq!(action.redirect, Some("/posts".to_string()));
190    }
191
192    #[test]
193    fn test_generate_action_url() {
194        let action = ParsedAction {
195            action_type: ActionType::Create,
196            collection: Some("posts".to_string()),
197            id: None,
198            redirect: Some("/posts".to_string()),
199            target: None,
200            swap: SwapMode::InnerHtml,
201            confirm: None,
202        };
203
204        let url = ActionHandler::generate_action_url(&action);
205        assert!(url.starts_with("/w-action/posts"));
206        assert!(url.contains("w-action=create"));
207    }
208}