acp/vars/
expander.rs

1//! @acp:module "Variable Expander"
2//! @acp:summary "Expands variable references with inheritance support"
3//! @acp:domain cli
4//! @acp:layer service
5
6use std::collections::{HashMap, HashSet};
7
8use super::{capitalize, estimate_tokens, VarEntry, VarResolver};
9
10/// @acp:summary "Expands variable references with caching"
11pub struct VarExpander {
12    resolver: VarResolver,
13    expansion_cache: HashMap<String, String>,
14}
15
16impl VarExpander {
17    /// Create a new expander from a resolver
18    pub fn new(resolver: VarResolver) -> Self {
19        Self {
20            resolver,
21            expansion_cache: HashMap::new(),
22        }
23    }
24
25    /// Expand a single variable
26    pub fn expand_var(
27        &mut self,
28        name: &str,
29        max_depth: usize,
30        visited: &mut HashSet<String>,
31    ) -> Option<String> {
32        // Check cache
33        if let Some(cached) = self.expansion_cache.get(name) {
34            return Some(cached.clone());
35        }
36
37        // Cycle detection
38        if visited.contains(name) {
39            return Some(format!("[CYCLE:${}]", name));
40        }
41
42        let var = self.resolver.get(name)?;
43        visited.insert(name.to_string());
44
45        let mut value = var.value.clone();
46
47        // Recursively expand nested references
48        if max_depth > 0 {
49            let refs = self.resolver.find_references(&value);
50            for r in refs.iter().rev() {
51                if let Some(expanded) = self.expand_var(&r.name, max_depth - 1, visited) {
52                    value = format!("{}{}{}", &value[..r.start], expanded, &value[r.end..]);
53                }
54            }
55        }
56
57        self.expansion_cache.insert(name.to_string(), value.clone());
58        Some(value)
59    }
60
61    /// Expand all variable references in text
62    pub fn expand_text(&mut self, text: &str, mode: ExpansionMode) -> ExpansionResult {
63        let refs = self.resolver.find_references(text);
64        let tokens_before = estimate_tokens(text);
65        let mut expanded = text.to_string();
66        let mut vars_expanded = Vec::new();
67        let mut vars_unresolved = Vec::new();
68        let mut chains = Vec::new();
69
70        // Process in reverse to preserve positions
71        for r in refs.iter().rev() {
72            if let Some(var) = self.resolver.get(&r.name).cloned() {
73                vars_expanded.push(r.name.clone());
74
75                // Track inheritance chain
76                let chain = self.get_inheritance_chain(&r.name);
77                chains.push(chain);
78
79                // Format based on mode
80                let replacement = self.format_var(&r.name, &var, r.modifier.as_deref(), mode);
81                expanded = format!(
82                    "{}{}{}",
83                    &expanded[..r.start],
84                    replacement,
85                    &expanded[r.end..]
86                );
87            } else {
88                vars_unresolved.push(r.name.clone());
89            }
90        }
91
92        ExpansionResult {
93            original: text.to_string(),
94            expanded,
95            vars_expanded,
96            vars_unresolved,
97            tokens_before,
98            tokens_after: estimate_tokens(text),
99            inheritance_chains: chains,
100        }
101    }
102
103    /// Get inheritance chain for a variable by traversing refs
104    pub fn get_inheritance_chain(&self, name: &str) -> InheritanceChain {
105        let mut chain = vec![name.to_string()];
106        let mut visited = HashSet::new();
107        visited.insert(name.to_string());
108
109        let has_cycle = self.build_chain(name, &mut chain, &mut visited);
110        let depth = chain.len() - 1;
111
112        InheritanceChain {
113            root: name.to_string(),
114            chain,
115            depth,
116            has_cycle,
117        }
118    }
119
120    /// Build inheritance chain by traversing refs recursively
121    /// Returns true if a cycle was detected
122    fn build_chain(
123        &self,
124        name: &str,
125        chain: &mut Vec<String>,
126        visited: &mut HashSet<String>,
127    ) -> bool {
128        if let Some(var) = self.resolver.get(name) {
129            for ref_name in &var.refs {
130                if visited.contains(ref_name) {
131                    // Cycle detected
132                    return true;
133                }
134                visited.insert(ref_name.clone());
135                chain.push(ref_name.clone());
136                if self.build_chain(ref_name, chain, visited) {
137                    return true;
138                }
139            }
140        }
141        false
142    }
143
144    fn format_var(
145        &mut self,
146        name: &str,
147        var: &VarEntry,
148        modifier: Option<&str>,
149        mode: ExpansionMode,
150    ) -> String {
151        // Handle modifier
152        if let Some(m) = modifier {
153            return match m {
154                "description" | "summary" => var.description.clone().unwrap_or_default(),
155                "value" => var.value.clone(),
156                "type" => var.var_type.to_string(),
157                _ => var.value.clone(),
158            };
159        }
160
161        // Handle expansion mode
162        match mode {
163            ExpansionMode::None => format!("${}", name),
164            ExpansionMode::Summary => var.description.clone().unwrap_or_else(|| name.to_string()),
165            ExpansionMode::Inline => {
166                let mut visited = HashSet::new();
167                self.expand_var(name, 3, &mut visited)
168                    .unwrap_or_else(|| var.value.clone())
169            }
170            ExpansionMode::Annotated => {
171                format!("**${}** → {}", name, self.humanize_value(&var.value))
172            }
173            ExpansionMode::Block => self.format_block(name, var),
174            ExpansionMode::Interactive => {
175                format!(
176                    r#"<acp-var name="{}" description="{}">{}</acp-var>"#,
177                    name,
178                    var.description.as_deref().unwrap_or(name),
179                    var.description.as_deref().unwrap_or(name)
180                )
181            }
182        }
183    }
184
185    fn format_block(&self, name: &str, var: &VarEntry) -> String {
186        let mut lines = Vec::new();
187        lines.push(format!(
188            "> **{}**: {}",
189            name,
190            var.description.as_deref().unwrap_or("")
191        ));
192        lines.push(">".to_string());
193        lines.push(format!("> - **type**: {}", var.var_type));
194        lines.push(format!("> - **value**: {}", var.value));
195        lines.join("\n")
196    }
197
198    fn humanize_value(&self, value: &str) -> String {
199        value
200            .split('|')
201            .filter_map(|part| {
202                let (key, val) = part.split_once(':')?;
203
204                Some(format!("{}: {}", capitalize(key), val))
205            })
206            .collect::<Vec<_>>()
207            .join(" | ")
208    }
209
210    /// Clear the expansion cache
211    pub fn clear_cache(&mut self) {
212        self.expansion_cache.clear();
213    }
214}
215
216/// @acp:summary "Variable expansion mode controlling output format"
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum ExpansionMode {
219    /// Keep $VAR as-is
220    None,
221    /// Replace with description only
222    Summary,
223    /// Replace with full value inline
224    Inline,
225    /// Show both: **$VAR** → value
226    Annotated,
227    /// Full formatted block
228    Block,
229    /// HTML-like markers for UI
230    Interactive,
231}
232
233/// @acp:summary "Result of variable expansion operation"
234#[derive(Debug, Clone)]
235pub struct ExpansionResult {
236    pub original: String,
237    pub expanded: String,
238    pub vars_expanded: Vec<String>,
239    pub vars_unresolved: Vec<String>,
240    pub tokens_before: usize,
241    pub tokens_after: usize,
242    pub inheritance_chains: Vec<InheritanceChain>,
243}
244
245/// @acp:summary "Inheritance chain for a variable"
246#[derive(Debug, Clone)]
247pub struct InheritanceChain {
248    pub root: String,
249    pub chain: Vec<String>,
250    pub depth: usize,
251    pub has_cycle: bool,
252}