next-rs-actions 0.3.0

Server Actions implementation for next.rs
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FormData {
    fields: HashMap<String, FormValue>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FormValue {
    Text(String),
    File(FileData),
    Multiple(Vec<String>),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileData {
    pub name: String,
    pub size: u64,
    pub content_type: String,
    pub data: Vec<u8>,
}

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

    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
        self.fields
            .insert(key.into(), FormValue::Text(value.into()));
    }

    pub fn set_multiple(&mut self, key: impl Into<String>, values: Vec<String>) {
        self.fields.insert(key.into(), FormValue::Multiple(values));
    }

    pub fn set_file(&mut self, key: impl Into<String>, file: FileData) {
        self.fields.insert(key.into(), FormValue::File(file));
    }

    pub fn get(&self, key: &str) -> Option<&str> {
        match self.fields.get(key) {
            Some(FormValue::Text(s)) => Some(s),
            _ => None,
        }
    }

    pub fn get_all(&self, key: &str) -> Vec<&str> {
        match self.fields.get(key) {
            Some(FormValue::Text(s)) => vec![s],
            Some(FormValue::Multiple(v)) => v.iter().map(|s| s.as_str()).collect(),
            _ => vec![],
        }
    }

    pub fn get_file(&self, key: &str) -> Option<&FileData> {
        match self.fields.get(key) {
            Some(FormValue::File(f)) => Some(f),
            _ => None,
        }
    }

    pub fn keys(&self) -> impl Iterator<Item = &String> {
        self.fields.keys()
    }

    pub fn to_json(&self) -> serde_json::Value {
        let mut map = serde_json::Map::new();
        for (key, value) in &self.fields {
            let json_value = match value {
                FormValue::Text(s) => serde_json::Value::String(s.clone()),
                FormValue::Multiple(v) => serde_json::Value::Array(
                    v.iter()
                        .map(|s| serde_json::Value::String(s.clone()))
                        .collect(),
                ),
                FormValue::File(f) => serde_json::json!({
                    "name": f.name,
                    "size": f.size,
                    "contentType": f.content_type,
                }),
            };
            map.insert(key.clone(), json_value);
        }
        serde_json::Value::Object(map)
    }
}

#[derive(Debug, Clone)]
pub struct FormAction {
    action_id: String,
    method: FormMethod,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormMethod {
    Post,
    Get,
}

impl FormAction {
    pub fn new(action_id: impl Into<String>) -> Self {
        Self {
            action_id: action_id.into(),
            method: FormMethod::Post,
        }
    }

    pub fn with_method(mut self, method: FormMethod) -> Self {
        self.method = method;
        self
    }

    pub fn action_id(&self) -> &str {
        &self.action_id
    }

    pub fn method(&self) -> FormMethod {
        self.method
    }

    pub fn action_url(&self) -> String {
        format!("/_actions/{}", self.action_id)
    }
}

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

    #[test]
    fn test_form_data_text() {
        let mut form = FormData::new();
        form.set("name", "John");
        form.set("email", "john@example.com");

        assert_eq!(form.get("name"), Some("John"));
        assert_eq!(form.get("email"), Some("john@example.com"));
        assert_eq!(form.get("missing"), None);
    }

    #[test]
    fn test_form_data_multiple() {
        let mut form = FormData::new();
        form.set_multiple("tags", vec!["rust".to_string(), "web".to_string()]);

        let tags = form.get_all("tags");
        assert_eq!(tags.len(), 2);
        assert!(tags.contains(&"rust"));
        assert!(tags.contains(&"web"));
    }

    #[test]
    fn test_form_data_file() {
        let mut form = FormData::new();
        form.set_file(
            "avatar",
            FileData {
                name: "photo.jpg".to_string(),
                size: 1024,
                content_type: "image/jpeg".to_string(),
                data: vec![1, 2, 3],
            },
        );

        let file = form.get_file("avatar").unwrap();
        assert_eq!(file.name, "photo.jpg");
        assert_eq!(file.size, 1024);
    }

    #[test]
    fn test_form_action() {
        let action = FormAction::new("create-post");
        assert_eq!(action.action_id(), "create-post");
        assert_eq!(action.method(), FormMethod::Post);
        assert_eq!(action.action_url(), "/_actions/create-post");
    }

    #[test]
    fn test_form_data_to_json() {
        let mut form = FormData::new();
        form.set("name", "Test");

        let json = form.to_json();
        assert_eq!(json["name"], "Test");
    }
}