acp/vars/
mod.rs

1//! @acp:module "Variables"
2//! @acp:summary "Variable system for token-efficient macros (schema-compliant)"
3//! @acp:domain cli
4//! @acp:layer model
5//! @acp:stability stable
6
7mod expander;
8mod resolver;
9
10pub mod presets;
11
12pub use expander::{ExpansionMode, ExpansionResult, InheritanceChain, VarExpander};
13pub use resolver::{VarReference, VarResolver};
14
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fs::File;
18use std::io::{BufReader, BufWriter};
19use std::path::Path;
20
21use crate::error::Result;
22
23fn default_vars_schema() -> String {
24    "https://acp-protocol.dev/schemas/v1/vars.schema.json".to_string()
25}
26
27/// @acp:summary "Complete vars file structure for .acp.vars.json (schema-compliant)"
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct VarsFile {
30    /// JSON Schema URL for validation
31    #[serde(rename = "$schema", default = "default_vars_schema")]
32    pub schema: String,
33    /// ACP specification version (required)
34    pub version: String,
35    /// Map of variable names to variable entries (required)
36    pub variables: HashMap<String, VarEntry>,
37}
38
39impl VarsFile {
40    /// Create a new empty vars file
41    pub fn new() -> Self {
42        Self {
43            schema: default_vars_schema(),
44            version: crate::VERSION.to_string(),
45            variables: HashMap::new(),
46        }
47    }
48
49    /// Load from JSON file
50    pub fn from_json<P: AsRef<Path>>(path: P) -> Result<Self> {
51        let file = File::open(path)?;
52        let reader = BufReader::new(file);
53        Ok(serde_json::from_reader(reader)?)
54    }
55
56    /// Write to JSON file
57    pub fn write_json<P: AsRef<Path>>(&self, path: P) -> Result<()> {
58        let file = File::create(path)?;
59        let writer = BufWriter::new(file);
60        serde_json::to_writer_pretty(writer, self)?;
61        Ok(())
62    }
63
64    /// Add a variable entry
65    pub fn add_variable(&mut self, name: String, entry: VarEntry) {
66        self.variables.insert(name, entry);
67    }
68}
69
70impl Default for VarsFile {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76/// @acp:summary "A single variable entry (schema-compliant)"
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct VarEntry {
79    /// Variable type (required)
80    #[serde(rename = "type")]
81    pub var_type: VarType,
82    /// Reference value - qualified name, path, etc. (required)
83    pub value: String,
84    /// Human-readable description (optional)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub description: Option<String>,
87    /// References to other variables for inheritance chains (optional)
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub refs: Vec<String>,
90    /// Source file path where the variable is defined (optional)
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub source: Option<String>,
93    /// Line range [start, end] in source file (optional)
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub lines: Option<[usize; 2]>,
96}
97
98impl VarEntry {
99    /// Create a new symbol variable
100    pub fn symbol(value: impl Into<String>, description: Option<String>) -> Self {
101        Self {
102            var_type: VarType::Symbol,
103            value: value.into(),
104            description,
105            refs: vec![],
106            source: None,
107            lines: None,
108        }
109    }
110
111    /// Create a new symbol variable with source location
112    pub fn symbol_with_source(
113        value: impl Into<String>,
114        description: Option<String>,
115        source: String,
116        lines: [usize; 2],
117    ) -> Self {
118        Self {
119            var_type: VarType::Symbol,
120            value: value.into(),
121            description,
122            refs: vec![],
123            source: Some(source),
124            lines: Some(lines),
125        }
126    }
127
128    /// Create a new symbol variable with refs (for inheritance)
129    pub fn symbol_with_refs(
130        value: impl Into<String>,
131        description: Option<String>,
132        refs: Vec<String>,
133    ) -> Self {
134        Self {
135            var_type: VarType::Symbol,
136            value: value.into(),
137            description,
138            refs,
139            source: None,
140            lines: None,
141        }
142    }
143
144    /// Create a new file variable
145    pub fn file(value: impl Into<String>, description: Option<String>) -> Self {
146        Self {
147            var_type: VarType::File,
148            value: value.into(),
149            description,
150            refs: vec![],
151            source: None,
152            lines: None,
153        }
154    }
155
156    /// Create a new domain variable
157    pub fn domain(value: impl Into<String>, description: Option<String>) -> Self {
158        Self {
159            var_type: VarType::Domain,
160            value: value.into(),
161            description,
162            refs: vec![],
163            source: None,
164            lines: None,
165        }
166    }
167
168    /// Create a new layer variable
169    pub fn layer(value: impl Into<String>, description: Option<String>) -> Self {
170        Self {
171            var_type: VarType::Layer,
172            value: value.into(),
173            description,
174            refs: vec![],
175            source: None,
176            lines: None,
177        }
178    }
179
180    /// Create a new pattern variable
181    pub fn pattern(value: impl Into<String>, description: Option<String>) -> Self {
182        Self {
183            var_type: VarType::Pattern,
184            value: value.into(),
185            description,
186            refs: vec![],
187            source: None,
188            lines: None,
189        }
190    }
191
192    /// Create a new context variable
193    pub fn context(value: impl Into<String>, description: Option<String>) -> Self {
194        Self {
195            var_type: VarType::Context,
196            value: value.into(),
197            description,
198            refs: vec![],
199            source: None,
200            lines: None,
201        }
202    }
203}
204
205/// @acp:summary "Variable type (schema-compliant)"
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(rename_all = "lowercase")]
208pub enum VarType {
209    Symbol,
210    File,
211    Domain,
212    Layer,
213    Pattern,
214    Context,
215}
216
217impl std::fmt::Display for VarType {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        let s = match self {
220            Self::Symbol => "symbol",
221            Self::File => "file",
222            Self::Domain => "domain",
223            Self::Layer => "layer",
224            Self::Pattern => "pattern",
225            Self::Context => "context",
226        };
227        write!(f, "{}", s)
228    }
229}
230
231/// @acp:summary "Estimate token count from text length"
232pub fn estimate_tokens(text: &str) -> usize {
233    text.len().div_ceil(4)
234}
235
236/// @acp:summary "Capitalize first character of string"
237pub fn capitalize(s: &str) -> String {
238    let mut c = s.chars();
239    match c.next() {
240        None => String::new(),
241        Some(f) => f.to_uppercase().chain(c).collect(),
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_find_references() {
251        let vars_file = VarsFile {
252            schema: default_vars_schema(),
253            version: "1.0.0".to_string(),
254            variables: HashMap::new(),
255        };
256        let resolver = VarResolver::new(vars_file);
257
258        let refs = resolver.find_references("Check $SYM_TEST and $ARCH_FLOW.value");
259        assert_eq!(refs.len(), 2);
260        assert_eq!(refs[0].name, "SYM_TEST");
261        assert_eq!(refs[1].name, "ARCH_FLOW");
262        assert_eq!(refs[1].modifier, Some("value".to_string()));
263    }
264
265    #[test]
266    fn test_vars_roundtrip() {
267        let mut vars_file = VarsFile::new();
268        vars_file.add_variable(
269            "SYM_TEST".to_string(),
270            VarEntry::symbol("test.rs:test_fn", Some("Test function".to_string())),
271        );
272
273        let json = serde_json::to_string_pretty(&vars_file).unwrap();
274        let parsed: VarsFile = serde_json::from_str(&json).unwrap();
275
276        assert!(parsed.variables.contains_key("SYM_TEST"));
277        assert_eq!(parsed.variables["SYM_TEST"].var_type, VarType::Symbol);
278    }
279}