greentic-start-dev 1.1.26572933845

Greentic lifecycle runner for start/restart/stop orchestration
Documentation
#![allow(dead_code)]

use std::path::Path;

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Policy {
    Public,
    Forbidden,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GmapPath {
    pub pack: Option<String>,
    pub flow: Option<String>,
    pub node: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GmapRule {
    pub path: GmapPath,
    pub policy: Policy,
    pub line: usize,
}

pub fn parse_file(path: &Path) -> anyhow::Result<Vec<GmapRule>> {
    if !path.exists() {
        return Ok(Vec::new());
    }
    let contents = std::fs::read_to_string(path)?;
    parse_str(&contents)
}

pub fn parse_str(contents: &str) -> anyhow::Result<Vec<GmapRule>> {
    let mut rules = Vec::new();
    for (idx, line) in contents.lines().enumerate() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let rule = parse_rule_line(line, idx + 1)?;
        rules.push(rule);
    }
    Ok(rules)
}

pub fn parse_rule_line(line: &str, line_number: usize) -> anyhow::Result<GmapRule> {
    let mut parts = line.splitn(2, '=');
    let raw_path = parts
        .next()
        .map(|part| part.trim())
        .filter(|part| !part.is_empty())
        .ok_or_else(|| anyhow::anyhow!("Invalid rule line {}: missing path", line_number))?;
    let raw_policy = parts
        .next()
        .map(|part| part.trim())
        .filter(|part| !part.is_empty())
        .ok_or_else(|| anyhow::anyhow!("Invalid rule line {}: missing policy", line_number))?;

    let path = parse_path(raw_path, line_number)?;
    let policy = parse_policy(raw_policy, line_number)?;
    Ok(GmapRule {
        path,
        policy,
        line: line_number,
    })
}

pub fn parse_path(raw: &str, line_number: usize) -> anyhow::Result<GmapPath> {
    if raw == "_" {
        return Ok(GmapPath {
            pack: None,
            flow: None,
            node: None,
        });
    }
    let mut segments = raw.split('/').filter(|seg| !seg.is_empty());
    let Some(pack) = segments.next() else {
        return Err(anyhow::anyhow!(
            "Invalid path on line {}: empty path",
            line_number
        ));
    };
    let flow = segments.next();
    let node = segments.next();
    if segments.next().is_some() {
        return Err(anyhow::anyhow!(
            "Invalid path on line {}: too many segments",
            line_number
        ));
    }
    Ok(GmapPath {
        pack: Some(pack.to_string()),
        flow: flow.map(str::to_string),
        node: node.map(str::to_string),
    })
}

pub fn parse_policy(raw: &str, line_number: usize) -> anyhow::Result<Policy> {
    match raw {
        "public" => Ok(Policy::Public),
        "forbidden" => Ok(Policy::Forbidden),
        other => Err(anyhow::anyhow!(
            "Invalid policy on line {}: {}",
            line_number,
            other
        )),
    }
}

impl std::fmt::Display for GmapPath {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match (&self.pack, &self.flow, &self.node) {
            (None, None, None) => write!(formatter, "_"),
            (Some(pack), None, None) => write!(formatter, "{pack}"),
            (Some(pack), Some(flow), None) => write!(formatter, "{pack}/{flow}"),
            (Some(pack), Some(flow), Some(node)) => write!(formatter, "{pack}/{flow}/{node}"),
            _ => write!(formatter, "_"),
        }
    }
}

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

    #[test]
    fn parse_file_missing_returns_empty_and_parse_str_skips_comments() {
        let dir = tempdir().expect("tempdir");
        let missing = dir.path().join("missing.gmap");
        assert!(parse_file(&missing).expect("missing file").is_empty());

        let rules = parse_str(
            r#"
            # comment

            _ = forbidden
            pack/flow = public
            "#,
        )
        .expect("parse");
        assert_eq!(rules.len(), 2);
        assert_eq!(rules[0].line, 4);
        assert_eq!(rules[1].path.flow.as_deref(), Some("flow"));
    }

    #[test]
    fn parse_rule_line_and_path_support_wildcard_and_segments() {
        let rule = parse_rule_line("pack/flow/node = public", 7).expect("rule");
        assert_eq!(rule.path.pack.as_deref(), Some("pack"));
        assert_eq!(rule.path.flow.as_deref(), Some("flow"));
        assert_eq!(rule.path.node.as_deref(), Some("node"));
        assert_eq!(rule.policy, Policy::Public);
        assert_eq!(rule.line, 7);

        let wildcard = parse_path("_", 1).expect("wildcard");
        assert_eq!(
            wildcard,
            GmapPath {
                pack: None,
                flow: None,
                node: None
            }
        );
    }

    #[test]
    fn parse_errors_are_reported_for_invalid_lines_paths_and_policies() {
        assert!(
            parse_rule_line("= public", 2)
                .expect_err("missing path")
                .to_string()
                .contains("missing path")
        );
        assert!(
            parse_rule_line("pack =", 3)
                .expect_err("missing policy")
                .to_string()
                .contains("missing policy")
        );
        assert!(
            parse_path("a/b/c/d", 4)
                .expect_err("too many segments")
                .to_string()
                .contains("too many segments")
        );
        assert!(
            parse_policy("private", 5)
                .expect_err("invalid policy")
                .to_string()
                .contains("Invalid policy")
        );
    }

    #[test]
    fn gmap_path_display_renders_supported_shapes() {
        assert_eq!(
            GmapPath {
                pack: None,
                flow: None,
                node: None
            }
            .to_string(),
            "_"
        );
        assert_eq!(
            GmapPath {
                pack: Some("pack".to_string()),
                flow: None,
                node: None
            }
            .to_string(),
            "pack"
        );
        assert_eq!(
            GmapPath {
                pack: Some("pack".to_string()),
                flow: Some("flow".to_string()),
                node: Some("node".to_string())
            }
            .to_string(),
            "pack/flow/node"
        );
    }
}