govctl 0.8.0

Project governance CLI for RFC, ADR, and Work Item management
//! Edit path rules generated from JSON SSOT (ADR-0030).

use crate::diagnostic::DiagnosticCode;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldKind {
    Scalar,
    List,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NestedNodeKind {
    Scalar,
    Object,
    List,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum NestedScalarMode {
    String,
    OptionalString {
        empty_as_null: bool,
    },
    Integer,
    Enum {
        allowed: &'static [&'static str],
        invalid_msg: &'static str,
        code: Option<DiagnosticCode>,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NestedChildRule {
    pub name: &'static str,
    pub node: &'static NestedNodeRule,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NestedNodeRule {
    pub kind: NestedNodeKind,
    pub verbs: &'static [&'static str],
    pub text_key: Option<&'static str>,
    pub set_mode: Option<NestedScalarMode>,
    pub item: Option<&'static NestedNodeRule>,
    pub fields: &'static [NestedChildRule],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NestedRootRule {
    pub artifact: &'static str,
    pub root: &'static str,
    pub content_path: &'static [&'static str],
    pub node: &'static NestedNodeRule,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimpleFieldRule {
    pub artifact: &'static str,
    pub name: &'static str,
    pub kind: FieldKind,
    pub verbs: &'static [&'static str],
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationKind {
    Semver,
    ClauseSupersededBy,
    ArtifactRef,
    EnumValue,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FieldValidationRule {
    pub artifact: &'static str,
    pub field: &'static str,
    pub kind: ValidationKind,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verb {
    Get,
    Set,
    Add,
    Remove,
    Tick,
}

impl Verb {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Get => "get",
            Self::Set => "set",
            Self::Add => "add",
            Self::Remove => "remove",
            Self::Tick => "tick",
        }
    }
}

macro_rules! define_alias_resolver {
    ($(($alias:literal, $canonical:literal)),* $(,)?) => {
        pub fn normalize_alias(name: &str) -> &str {
            match name {
                $($alias => $canonical,)*
                _ => name,
            }
        }
    };
}

macro_rules! define_legacy_prefix_resolver {
    ($(($prefix:literal, [$($field:literal),* $(,)?])),* $(,)?) => {
        pub fn can_collapse_legacy_prefix(prefix: &str, field: &str) -> bool {
            match prefix {
                $($prefix => matches!(field, $($field)|*),)*
                _ => false,
            }
        }
    };
}

include!(concat!(env!("OUT_DIR"), "/edit_rules_generated.rs"));

pub fn nested_root_rule(artifact: &str, root: &str) -> Option<&'static NestedRootRule> {
    NESTED_RULES
        .iter()
        .find(|rule| rule.artifact == artifact && rule.root == root)
}

pub fn simple_field_rule(artifact: &str, field: &str) -> Option<&'static SimpleFieldRule> {
    SIMPLE_RULES
        .iter()
        .find(|rule| rule.artifact == artifact && rule.name == field)
}

pub fn simple_field_supports_verb(artifact: &str, field: &str, verb: Verb) -> bool {
    simple_field_rule(artifact, field).is_some_and(|rule| rule.verbs.contains(&verb.as_str()))
}

pub fn nested_field_rule(
    artifact: &str,
    root: &str,
    field: &str,
) -> Option<&'static NestedChildRule> {
    let rule = nested_root_rule(artifact, root)?;
    match rule.node.kind {
        NestedNodeKind::Object => rule.node.fields.iter().find(|f| f.name == field),
        NestedNodeKind::List => {
            let item = rule.node.item?;
            if item.kind != NestedNodeKind::Object {
                return None;
            }
            item.fields.iter().find(|f| f.name == field)
        }
        NestedNodeKind::Scalar => None,
    }
}

#[cfg(test)]
pub fn nested_field_supports_verb(artifact: &str, root: &str, field: &str, verb: Verb) -> bool {
    nested_field_rule(artifact, root, field)
        .is_some_and(|rule| rule.node.verbs.contains(&verb.as_str()))
}

pub fn field_validation_rule(artifact: &str, field: &str) -> Option<&'static FieldValidationRule> {
    VALIDATION_RULES
        .iter()
        .find(|rule| rule.artifact == artifact && rule.field == field)
}

pub fn field_validation_kind(artifact: &str, field: &str) -> Option<ValidationKind> {
    field_validation_rule(artifact, field).map(|rule| rule.kind)
}

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

    #[test]
    fn test_aliases_generated() {
        assert_eq!(normalize_alias("alt"), "alternatives");
        assert_eq!(normalize_alias("reason"), "rejection_reason");
        assert_eq!(normalize_alias("unknown"), "unknown");
    }

    #[test]
    fn test_legacy_prefix_generation() {
        assert!(can_collapse_legacy_prefix("content", "decision"));
        assert!(!can_collapse_legacy_prefix("content", "nonexistent"));
        assert!(!can_collapse_legacy_prefix("nope", "decision"));
    }

    #[test]
    fn test_nested_rule_lookup() {
        let rule = nested_root_rule("adr", "alternatives").expect("rule should exist");
        assert_eq!(rule.node.kind, NestedNodeKind::List);
        assert_eq!(EDIT_RULES_VERSION, 2);
    }

    #[test]
    fn test_nested_field_supports_verb() {
        assert!(nested_field_supports_verb(
            "adr",
            "alternatives",
            "pros",
            Verb::Add
        ));
        assert!(!nested_field_supports_verb(
            "adr",
            "alternatives",
            "status",
            Verb::Add
        ));
    }

    #[test]
    fn test_nested_object_root_lookup() {
        let rule = nested_root_rule("guard", "check").expect("rule should exist");
        assert_eq!(rule.node.kind, NestedNodeKind::Object);
        let child = nested_field_rule("guard", "check", "timeout_secs").expect("child exists");
        assert_eq!(child.node.kind, NestedNodeKind::Scalar);
    }

    #[test]
    fn test_simple_field_supports_verb() {
        assert!(simple_field_supports_verb("adr", "alternatives", Verb::Add));
        assert!(!simple_field_supports_verb(
            "adr",
            "superseded_by",
            Verb::Set
        ));
    }

    #[test]
    fn test_validation_rule_lookup() {
        assert_eq!(
            field_validation_kind("rfc", "version"),
            Some(ValidationKind::Semver)
        );
        assert_eq!(
            field_validation_kind("clause", "superseded_by"),
            Some(ValidationKind::ClauseSupersededBy)
        );
        assert_eq!(field_validation_kind("rfc", "owners"), None);
    }
}