nova-forms 0.1.11

Build online forms with ease.
Documentation
use std::collections::{BTreeMap, HashMap};

use leptos::*;
use serde::{de::DeserializeOwned, Serialize};

use crate::{QueryString, QueryStringPart};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FormData(RwSignal<Data>);

impl FormData {
    pub fn new() -> Self {
        Self(RwSignal::new(Data::new_group()))
    }

    pub fn from_data(data: Data) -> Self {
        Self(RwSignal::new(data))
    }

    pub fn get(&self, qs: QueryString) -> Signal<Option<Data>> {
        let self_singal = self.0;
        Memo::new(move |_| {
            self_singal.get().get(qs)
        }).into()
    }

    pub fn set(&self, qs: QueryString, value: Data) {
        self.0.update(|data| {
            data.set(qs, value);
        });
    }

    pub fn from_urlencoded(data: &str) -> Self {
        Self::from_data(Data::from_urlencoded(data))
    }

    pub fn from<T: Serialize>(value: &T) -> Self {
        Self::from_data(Data::from(value))
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Data {
    Group(GroupData),
    Input(InputData),
}

impl Data {
    pub fn new_group() -> Self {
        Data::Group(GroupData::new())
    }

    pub fn new_input(value: String) -> Self {
        Data::Input(InputData::new(value))
    }

    fn from_vec(data: Vec<(QueryString, String)>) -> Self {
        let mut parts = HashMap::new();

        for (k, v) in data {
            if let Some(head) = k.first() {
                let vec: &mut Vec<(QueryString, String)> = parts
                    .entry(head)
                    .or_default();

                vec.push((k.remove_first(), v));
            } else {
                return Data::Input(InputData(v));
            }
        }

        let group = parts.into_iter()
            .map(|(head, v)| {
                (head, Data::from_vec(v))
            })
            .collect();

            Data::Group(GroupData(group))
    }

    fn to_vec(&self) -> Vec<(QueryString, String)> {
        fn visit(data: &Data, qs: QueryString, acc: &mut Vec<(QueryString, String)>) {
            match data {
                Data::Group(group) => {
                    for (head, data) in &group.0 {
                        visit(data, qs.add(*head), acc);
                    }
                }
                Data::Input(value) => {
                    acc.push((qs, value.0.clone()));
                }
            }
        }

        let mut acc = Vec::new();
        visit(self, QueryString::default(), &mut acc);
        acc
    }

    pub fn from_urlencoded(data: &str) -> Self {
        let vec = data.split('&').into_iter()
            .map(|part| part.split_once('=').map(|(k, v)| {
                (QueryString::from(k), v.to_owned())
            }).unwrap_or((QueryString::from(part), String::new())))
            .collect::<Vec<_>>();

        Self::from_vec(vec)
    }

    pub fn to_urlencoded(&self) -> String {
        self.to_vec().into_iter()
            .map(|(k, v)| {
                format!("{}={}", k.to_string(), v)
            })
            .collect::<Vec<_>>()
            .join("&")
    }

    pub fn to<T: DeserializeOwned>(&self) -> Result<T, serde_qs::Error> {
        serde_qs::from_str(&self.to_urlencoded())
    }

    pub fn from<T: Serialize>(value: &T) -> Self {
        let data = serde_qs::to_string(value).expect("failed to serialize form data");
        Data::from_urlencoded(&data)
    }

    pub fn get(&self, qs: QueryString) -> Option<Data> {
        if let Some(first) = qs.first() {
            if let Data::Group(group) = self {
                group.0.get(&first)?.get(qs.remove_first())
            } else {
                None
            }
        } else {
            Some(self.clone())
        }
    }

    pub fn set(&mut self, qs: QueryString, value: Data) {
        if let Some(first) = qs.first() {
            if let Data::Group(group) = self {
                let data = group.0.entry(first).or_insert_with(|| Data::new_group());
                data.set(qs.remove_first(), value);
            }
        } else {
            *self = value;
        }
    }

    pub fn len(&self) -> Option<usize> {
        match self {
            Data::Group(group) => Some(group.len()),
            _ => None,
        }
    }

    pub fn as_input(&self) -> Option<&InputData> {
        match self {
            Data::Input(value) => Some(value),
            _ => None,
        }
    }

    pub fn as_group(&self) -> Option<&GroupData> {
        match self {
            Data::Group(group) => Some(group),
            _ => None,
        }
    }

    pub fn into_input(self) -> Option<InputData> {
        match self {
            Data::Input(value) => Some(value),
            _ => None,
        }
    }

    pub fn into_group(self) -> Option<GroupData> {
        match self {
            Data::Group(group) => Some(group),
            _ => None,
        }
    }

}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GroupData(BTreeMap<QueryStringPart, Data>);

impl GroupData {
    pub fn new() -> Self {
        Self(BTreeMap::new())
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InputData(String);

impl InputData {
    pub fn new(value: String) -> Self {
        Self(value)
    }

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


#[cfg(test)]
mod tests {
    use serde::Deserialize;

    use crate::qs;
    use super::*;

    #[test]
    fn test_from_urlencoded() {
        let _ = leptos::create_runtime();

        let form_data = FormData::from_urlencoded("a=1&b=2&c=3");
        assert_eq!(form_data.get(qs!(a)).get_untracked().unwrap().as_input().unwrap().raw(), "1");
        assert_eq!(form_data.get(qs!(b)).get_untracked().unwrap().as_input().unwrap().raw(), "2");
        assert_eq!(form_data.get(qs!(c)).get_untracked().unwrap().as_input().unwrap().raw(), "3");
    }

    #[test]
    fn test_to_urlencoded() {
        let _ = leptos::create_runtime();

        let form_data = FormData::from_urlencoded("a=1&b=2&c=3");
        assert_eq!(form_data.get(qs!(a)).get_untracked().unwrap().as_input().unwrap().raw(), "1");
        assert_eq!(form_data.get(qs!(b)).get_untracked().unwrap().as_input().unwrap().raw(), "2");
        assert_eq!(form_data.get(qs!(c)).get_untracked().unwrap().as_input().unwrap().raw(), "3");
        assert_eq!(form_data.get(qs!()).get_untracked().unwrap().to_urlencoded(), "a=1&b=2&c=3");
    }

    
    #[test]
    fn test_set_value() {
        let _ = leptos::create_runtime();

        let form_data = FormData::from_urlencoded("a=1&b=2&c=3");
        form_data.set(qs!(b), Data::new_input("7".to_owned()));
        assert_eq!(form_data.get(qs!(a)).get_untracked().unwrap().as_input().unwrap().raw(), "1");
        assert_eq!(form_data.get(qs!(b)).get_untracked().unwrap().as_input().unwrap().raw(), "7");
        assert_eq!(form_data.get(qs!(c)).get_untracked().unwrap().as_input().unwrap().raw(), "3");

    }

    #[test]
    fn test_values() {
        let _ = leptos::create_runtime();

        #[derive(Deserialize, PartialEq, Eq, Debug, Clone)]
        struct Test {
            a: i32,
            b: i32,
            c: i32,
        }

        let form_data = FormData::from_urlencoded("a=1&b=2&c=3");
        assert_eq!(form_data.get(qs!()).get_untracked().unwrap().to::<Test>().unwrap(), Test { a: 1, b: 2, c: 3 });
    }

    #[test]
    fn test_set_values() {
        let _ = leptos::create_runtime();

        #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
        struct Test {
            a: i32,
            b: i32,
            c: i32,
        }

        let form_data = FormData::from_urlencoded("a=1&b=2&c=3");
        form_data.set(qs!(), Data::from(&Test { a: 4, b: 5, c: 6 }));
        assert_eq!(form_data.get(qs!()).get_untracked().unwrap().to::<Test>().unwrap(), Test { a: 4, b: 5, c: 6 });
    }

    #[test]
    fn test_len() {
        let _ = leptos::create_runtime();

        let form_data = FormData::from_urlencoded("a[0]=1&a[3]=21&a[2]=23&a[1]=3");
        assert_eq!(form_data.get(qs!(a)).get_untracked().unwrap().as_group().unwrap().len(), 4);
    }
}