instruct 0.1.0

A language to write general purpose 'makefile like' tasks which are powerful and reuseable
Documentation
use std::collections::HashMap;

use anyhow::Context;
use thiserror::Error;

use crate::parse::ast::{Executeable, Namespace, NamespaceOrExecuteable};

#[derive(Error, Debug, PartialEq, Eq)]
pub enum NamespaceError {
    #[error("searching '{0}' as '{1}' is invalid and should never happen!")]
    InvalidSearch(String, String),
    #[error("could not find namespace '{0}' in '{1}'")]
    NotFound(String, String),
    #[error("the referenced namespace '{0}' is a executeable and therefor not a namespace")]
    NotANamespace(String),
    #[error("the referenced task '{0}' is not a executeable")]
    NotAExecuteable(String),
}

#[derive(Debug)]
pub struct NamespaceResolver<'a> {
    namespace: &'a Namespace,
}

impl<'a> NamespaceResolver<'a> {
    pub fn new(namespace: &'a Namespace) -> Self {
        NamespaceResolver { namespace }
    }

    pub fn resolve(&self, name_parts: &[&str]) -> anyhow::Result<&'a Executeable> {
        let current_part: &str = match name_parts.get(0) {
            Some(val) => *val,
            None => return Err(NamespaceError::NotAExecuteable(self.namespace.name.clone()).into()),
        };

        if current_part != self.namespace.name {
            return Err(NamespaceError::InvalidSearch(
                current_part.into(),
                self.namespace.name.clone(),
            )
            .into());
        }

        let next_part: &str = match name_parts.get(1) {
            Some(val) => *val,
            None => return Err(NamespaceError::NotAExecuteable(self.namespace.name.clone()).into()),
        };

        match self.namespace.children.get(next_part) {
            Some(NamespaceOrExecuteable::Namespace(next)) => Ok(NamespaceResolver::new(next)
                .resolve(&name_parts[1..])
                .with_context(|| {
                    format!("at searching '{}' in '{}'", next_part, &self.namespace.name)
                })?),
            Some(NamespaceOrExecuteable::Executeable(executeable)) => {
                if name_parts.len() > 2 {
                    return Err(NamespaceError::NotANamespace(next_part.into()).into());
                }
                Ok(executeable)
            }
            None => Err(NamespaceError::NotFound(next_part.into(), current_part.into()).into()),
        }
    }
}

#[derive(Error, Debug)]
pub enum RootNamespaceError {
    #[error("The module name '{0}' is already used")]
    ModuleNameAlreadyUsed(String),
    #[error("A module with the name '{0}' could not be found")]
    ModuleNotFound(String),
    #[error("Tried to search an empty name, this should not happen")]
    EmptySearchName,
}

#[derive(Clone)]
pub struct RootNamespace {
    namespaces: HashMap<String, Namespace>,
}

impl RootNamespace {
    pub fn new() -> RootNamespace {
        RootNamespace {
            namespaces: HashMap::new(),
        }
    }

    pub fn add_root(&mut self, namespace: Namespace) -> anyhow::Result<()> {
        if self.namespaces.contains_key(&namespace.name) {
            return Err(RootNamespaceError::ModuleNameAlreadyUsed(namespace.name).into());
        }
        assert!(self
            .namespaces
            .insert(namespace.name.clone(), namespace)
            .is_none());

        Ok(())
    }

    pub fn resolve_name(&self, target_name: &str) -> anyhow::Result<&Executeable> {
        let target_name_vec: Vec<&str> = target_name.split('.').collect();
        self.resolve(&target_name_vec)
    }

    pub fn resolve(&self, name_parts: &[&str]) -> anyhow::Result<&Executeable> {
        if let Some(module_name) = name_parts.get(0) {
            match self.namespaces.get(*module_name) {
                Some(namespace) => NamespaceResolver::new(namespace).resolve(name_parts),
                None => Err(RootNamespaceError::ModuleNotFound((*module_name).into()).into()),
            }
        } else {
            Err(RootNamespaceError::EmptySearchName.into())
        }
    }
}

impl Default for RootNamespace {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use crate::interpreter::namespace::NamespaceError;
    use crate::parse::ast::Executeable;
    use crate::parse::ast::ExecuteableType;
    use crate::parse::ast::Namespace;
    use crate::parse::ast::NamespaceOrExecuteable;
    use crate::parse::ast::NamespaceType;

    use super::NamespaceResolver;

    fn get_collection(name: &'static str, mut children: Vec<NamespaceOrExecuteable>) -> Namespace {
        Namespace {
            name: name.into(),
            namespace_type: NamespaceType::Collection,
            children: children
                .drain(..)
                .map(|val| (val.get_name().to_owned(), val))
                .collect(),
        }
    }

    fn get_executeable(name: &'static str) -> NamespaceOrExecuteable {
        NamespaceOrExecuteable::Executeable(Executeable {
            output_variables: None,
            name: name.into(),
            options: None,
            executeable_type: ExecuteableType::Command { cmd: "".into() },
        })
    }

    fn split(value: &'static str) -> Vec<&str> {
        value.split(".").collect()
    }

    #[test]
    fn ok_resolve_1_depth() {
        let namespace = get_collection("root", vec![get_executeable("task")]);

        let name = split("root.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_ok());
        let task = res.unwrap();

        assert_eq!(&task.name, "task");
    }

    #[test]
    fn ok_resolve_1_depth_multiple_childs() {
        let namespace = get_collection(
            "root",
            vec![get_executeable("task"), get_executeable("other-task")],
        );

        let name = split("root.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_ok());
        let task = res.unwrap();

        assert_eq!(&task.name, "task");
    }

    #[test]
    fn ok_resolve_2_depth() {
        let namespace = get_collection(
            "root",
            vec![NamespaceOrExecuteable::Namespace(get_collection(
                "collection",
                vec![get_executeable("task")],
            ))],
        );

        let name = split("root.collection.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_ok());
        let task = res.unwrap();

        assert_eq!(&task.name, "task");
    }

    #[test]
    fn ok_resolve_4_depth() {
        let namespace = get_collection(
            "root",
            vec![NamespaceOrExecuteable::Namespace(get_collection(
                "collection1",
                vec![NamespaceOrExecuteable::Namespace(get_collection(
                    "collection2",
                    vec![NamespaceOrExecuteable::Namespace(get_collection(
                        "collection3",
                        vec![get_executeable("task")],
                    ))],
                ))],
            ))],
        );

        let name = split("root.collection1.collection2.collection3.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_ok());
        let task = res.unwrap();

        assert_eq!(&task.name, "task");
    }

    #[test]
    fn ok_resolve_4_depth_multiple_childs() {
        let namespace = get_collection(
            "root",
            vec![
                get_executeable("other-task"),
                NamespaceOrExecuteable::Namespace(get_collection(
                    "collection1",
                    vec![
                        NamespaceOrExecuteable::Namespace(get_collection(
                            "collection2",
                            vec![
                                get_executeable("other-task"),
                                get_executeable("another-task"),
                                NamespaceOrExecuteable::Namespace(get_collection(
                                    "collection3",
                                    vec![
                                        get_executeable("other-task"),
                                        get_executeable("another-task"),
                                        get_executeable("task"),
                                    ],
                                )),
                            ],
                        )),
                        get_executeable("other-task"),
                    ],
                )),
            ],
        );

        let name = split("root.collection1.collection2.collection3.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_ok());
        let task = res.unwrap();

        assert_eq!(&task.name, "task");
    }

    #[test]
    fn nok_task_is_collection() {
        let namespace = get_collection(
            "root",
            vec![
                get_executeable("other-task"),
                NamespaceOrExecuteable::Namespace(get_collection("collection", vec![])),
            ],
        );

        let name = split("root.collection");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_err());

        assert_eq!(
            res.unwrap_err().downcast::<NamespaceError>().unwrap(),
            NamespaceError::NotAExecuteable("collection".into())
        );
    }

    #[test]
    fn nok_collection_is_executeable() {
        let namespace = get_collection("root", vec![get_executeable("collection")]);

        let name = split("root.collection.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_err());

        assert_eq!(
            res.unwrap_err().downcast::<NamespaceError>().unwrap(),
            NamespaceError::NotANamespace("collection".into())
        );
    }

    #[test]
    fn nok_not_found_other_name() {
        let namespace = get_collection("root", vec![get_executeable("other-task")]);

        let name = split("root.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_err());

        assert_eq!(
            res.unwrap_err().downcast::<NamespaceError>().unwrap(),
            NamespaceError::NotFound("task".into(), "root".into())
        );
    }

    #[test]
    fn nok_not_found_empty() {
        let namespace = get_collection("root", vec![]);

        let name = split("root.task");
        let res = NamespaceResolver::new(&namespace).resolve(&name);

        assert!(res.is_err());

        assert_eq!(
            res.unwrap_err().downcast::<NamespaceError>().unwrap(),
            NamespaceError::NotFound("task".into(), "root".into())
        );
    }
}