vibe-action 0.0.4

Command router — execute shell commands and LLM prompts via simple YAML actions.
//! Modifier trait, registry, and application logic.
//! Each modifier transforms a ContextModel value via the pipe syntax: {tag|modifier:arg}

use crate::models::context::ContextModel;
use anyhow::Result;
use std::collections::HashMap;

/// Enum of all available modifier keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ModifierKey {
    Ast,
    Contains,
    Empty,
    Equals,
    IsDir,
    IsFile,
    Join,
    Lower,
    Resolve,
    Reverse,
    Size,
    Sort,
    Split,
    Take,
    Trim,
    Upper,
}

impl ModifierKey {
    /// Convert from string, returns None if unknown.
    pub fn from_str(s: &str) -> Option<Self> {
        match s {
            "ast" => Some(Self::Ast),
            "contains" => Some(Self::Contains),
            "empty" => Some(Self::Empty),
            "equals" => Some(Self::Equals),
            "is_dir" => Some(Self::IsDir),
            "is_file" => Some(Self::IsFile),
            "join" => Some(Self::Join),
            "lower" => Some(Self::Lower),
            "resolve" => Some(Self::Resolve),
            "reverse" => Some(Self::Reverse),
            "size" => Some(Self::Size),
            "sort" => Some(Self::Sort),
            "split" => Some(Self::Split),
            "take" => Some(Self::Take),
            "trim" => Some(Self::Trim),
            "upper" => Some(Self::Upper),
            _ => None,
        }
    }
}

/// Trait for pipe modifiers.
pub trait Modifier: Send + Sync {
    fn key(&self) -> ModifierKey;
    fn apply(&self, value: &ContextModel, arg: &str) -> Result<ContextModel>;
}

/// Registry of all modifiers.
pub struct ModifierRegistry {
    modifiers: HashMap<ModifierKey, Box<dyn Modifier>>,
}

impl ModifierRegistry {
    /// Create a new registry with all built-in modifiers.
    pub fn new() -> Self {
        let mut registry = Self {
            modifiers: HashMap::new(),
        };
        registry.register(Box::new(super::ast::AstModifier));
        registry.register(Box::new(super::contains::ContainsModifier));
        registry.register(Box::new(super::empty::EmptyModifier));
        registry.register(Box::new(super::equals::EqualsModifier));
        registry.register(Box::new(super::is_dir::IsDirModifier));
        registry.register(Box::new(super::is_file::IsFileModifier));
        registry.register(Box::new(super::join::JoinModifier));
        registry.register(Box::new(super::lower::LowerModifier));
        registry.register(Box::new(super::resolve::ResolveModifier));
        registry.register(Box::new(super::reverse::ReverseModifier));
        registry.register(Box::new(super::size::SizeModifier));
        registry.register(Box::new(super::sort::SortModifier));
        registry.register(Box::new(super::split::SplitModifier));
        registry.register(Box::new(super::take::TakeModifier));
        registry.register(Box::new(super::trim::TrimModifier));
        registry.register(Box::new(super::upper::UpperModifier));
        registry
    }

    /// Register a new modifier.
    pub fn register(&mut self, modifier: Box<dyn Modifier>) {
        self.modifiers.insert(modifier.key(), modifier);
    }

    /// Get a modifier by its key.
    pub fn get(&self, key: ModifierKey) -> Option<&dyn Modifier> {
        self.modifiers.get(&key).map(|m| m.as_ref())
    }

    /// Apply a chain of pipe modifiers to a value.
    /// Format: "mod1:arg1|mod2:arg2|mod3"
    pub fn apply_modifier(&self, modifier: &str, value: &ContextModel) -> Result<ContextModel> {
        if modifier.is_empty() {
            return Ok(value.clone());
        }
        let mut current = value.clone();
        for part in modifier.split('|') {
            let (name, arg) = part.split_once(':').unwrap_or((part, ""));
            match ModifierKey::from_str(name) {
                Some(key) => {
                    if let Some(modifier) = self.get(key) {
                        current = modifier.apply(&current, arg)?;
                    }
                }
                None => {}
            }
        }
        Ok(current)
    }
}

/// Invert a ContextModel::Bool or ContextModel::List<Bool>.
pub fn invert_bool(value: ContextModel) -> ContextModel {
    match value {
        ContextModel::Bool(b) => ContextModel::Bool(!b),
        ContextModel::List(items) => {
            let inverted: Vec<ContextModel> = items
                .iter()
                .map(|i| match i {
                    ContextModel::Bool(b) => ContextModel::Bool(!b),
                    _ => i.clone(),
                })
                .collect();
            ContextModel::List(inverted)
        }
        _ => value,
    }
}