acts 0.17.2

a fast, lightweight, extensiable workflow engine
Documentation
use crate::{ActError, Vars};
use core::fmt;
use serde::{Deserialize, Serialize, de};
use serde_json::Value;
use std::collections::HashMap;

#[derive(Debug, Default, Clone, Serialize, Deserialize, strum::AsRefStr)]
pub enum OutputType {
    #[default]
    String,
    Bool,
    Number,
    Array,
    Object,
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Output {
    pub required: bool,
    pub default: Value,
    pub r#type: OutputType,
}

#[derive(Debug, Default, Clone)]
pub struct Outputs {
    inner: HashMap<String, Output>,
}

fn get<T>(name: &str, value: &Value) -> Option<T>
where
    T: for<'de> Deserialize<'de> + Clone,
{
    if let Some(v) = value.get(name) {
        if let Ok(v) = serde_json::from_value::<T>(v.clone()) {
            return Some(v);
        }
    }

    None
}

impl fmt::Display for OutputType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_ref())
    }
}

impl From<Value> for Output {
    fn from(value: Value) -> Self {
        let required = get::<bool>("required", &value).unwrap_or_default();
        let default = get::<Value>("default", &value).unwrap_or_default();
        let r#type = get::<OutputType>("type", &value).unwrap_or_default();

        Self {
            required,
            default,
            r#type,
        }
    }
}

impl Serialize for Outputs {
    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        use serde::ser::SerializeMap;

        let mut map = serializer.serialize_map(Some(self.inner.len()))?;
        for (k, v) in &self.inner {
            map.serialize_entry(k, v)?;
        }
        map.end()
    }
}

impl<'de> Deserialize<'de> for Outputs {
    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct Visitor;

        impl<'de> de::Visitor<'de> for Visitor {
            type Value = HashMap<String, Output>;
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a map")
            }

            #[inline]
            fn visit_unit<E>(self) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                Ok(HashMap::new())
            }

            #[inline]
            fn visit_map<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
            where
                V: de::MapAccess<'de>,
            {
                let mut values = HashMap::new();

                while let Some((key, value)) = visitor.next_entry::<String, Value>()? {
                    let value: Output = value.into();
                    values.insert(key, value);
                }

                Ok(values)
            }
        }

        core::result::Result::Ok(Self {
            inner: deserializer.deserialize_map(Visitor)?,
        })
    }
}

impl Outputs {
    pub fn push(&mut self, name: &str, output: &Output) {
        self.inner.insert(name.to_string(), output.clone());
    }

    pub fn check(&self, vars: &Vars) -> crate::Result<()> {
        for (k, output) in &self.inner {
            let v = vars.get::<Value>(k);
            if v.is_none() && output.required {
                return Err(ActError::Runtime(format!("the key '{k}' is required",)));
            }

            if let Some(v) = &v {
                let is_type_match = match output.r#type {
                    OutputType::String => v.is_string(),
                    OutputType::Bool => v.is_boolean(),
                    OutputType::Number => v.is_number(),
                    OutputType::Array => v.is_array(),
                    OutputType::Object => v.is_object(),
                };

                if !is_type_match {
                    return Err(ActError::Runtime(format!(
                        "the value {k}({v}) is not matched the type '{}'",
                        output.r#type
                    )));
                }
            }
        }
        Ok(())
    }
}