aether_core/
slot.rs

1//! Slot definitions for code injection points.
2//!
3//! Slots are placeholders in templates where AI-generated code will be injected.
4
5use serde::{Deserialize, Serialize};
6
7/// Represents a slot in a template where code can be injected.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct Slot {
10    /// Unique identifier for this slot.
11    pub name: String,
12
13    /// The prompt or instruction for AI to generate code.
14    pub prompt: String,
15
16    /// Type of slot (determines generation behavior).
17    pub kind: SlotKind,
18
19    /// Optional constraints on generated code.
20    pub constraints: Option<SlotConstraints>,
21
22    /// Whether this slot is required.
23    pub required: bool,
24
25    /// Default value if AI generation fails.
26    pub default: Option<String>,
27
28    /// Specific temperature override for this slot (0.0 - 2.0).
29    pub temperature: Option<f32>,
30}
31
32/// The kind of slot determines how code is generated.
33#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
34#[serde(rename_all = "snake_case")]
35pub enum SlotKind {
36    /// Raw code injection (no wrapper).
37    #[default]
38    Raw,
39
40    /// Function definition.
41    Function,
42
43    /// Class/struct definition.
44    Class,
45
46    /// HTML element.
47    Html,
48
49    /// CSS styles.
50    Css,
51
52    /// JavaScript code.
53    JavaScript,
54
55    /// Complete component (HTML + CSS + JS).
56    Component,
57
58    /// Custom kind with user-defined wrapper.
59    Custom(String),
60}
61
62/// Constraints on generated code.
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
64pub struct SlotConstraints {
65    /// Maximum lines of code.
66    pub max_lines: Option<usize>,
67
68    /// Maximum characters.
69    pub max_chars: Option<usize>,
70
71    /// Required imports or dependencies.
72    pub required_imports: Vec<String>,
73
74    /// Forbidden patterns (regex).
75    pub forbidden_patterns: Vec<String>,
76
77    /// Language hint for code generation.
78    pub language: Option<String>,
79
80    /// TDD Test harness. This is the code that will be used to test the generated output.
81    /// It should contain a placeholder like `{{CODE}}` where the generated code will be injected.
82    pub test_harness: Option<String>,
83
84    /// Command to execute the test harness (e.g., "cargo test", "node test.js").
85    pub test_command: Option<String>,
86}
87
88impl Eq for Slot {}
89
90impl std::hash::Hash for Slot {
91    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
92        self.name.hash(state);
93        self.prompt.hash(state);
94        self.kind.hash(state);
95        self.constraints.hash(state);
96        self.required.hash(state);
97        self.default.hash(state);
98        if let Some(temp) = self.temperature {
99            temp.to_bits().hash(state);
100        }
101    }
102}
103
104impl Slot {
105    /// Create a new slot with the given name and prompt.
106    ///
107    /// # Arguments
108    ///
109    /// * `name` - Unique identifier for this slot
110    /// * `prompt` - The instruction for AI to generate code
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// use aether_core::Slot;
116    ///
117    /// let slot = Slot::new("button", "Create a submit button with hover effects");
118    /// assert_eq!(slot.name, "button");
119    /// ```
120    pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
121        Self {
122            name: name.into(),
123            prompt: prompt.into(),
124            kind: SlotKind::default(),
125            constraints: None,
126            required: true,
127            default: None,
128            temperature: None,
129        }
130    }
131
132    /// Set a specific temperature for this slot.
133    pub fn with_temperature(mut self, temp: f32) -> Self {
134        self.temperature = Some(temp.clamp(0.0, 2.0));
135        self
136    }
137
138    /// Set the slot kind.
139    pub fn with_kind(mut self, kind: SlotKind) -> Self {
140        self.kind = kind;
141        self
142    }
143
144    /// Set constraints on the generated code.
145    pub fn with_constraints(mut self, constraints: SlotConstraints) -> Self {
146        self.constraints = Some(constraints);
147        self
148    }
149
150    /// Mark this slot as optional with a default value.
151    pub fn optional(mut self, default: impl Into<String>) -> Self {
152        self.required = false;
153        self.default = Some(default.into());
154        self
155    }
156
157    /// Validate the generated code against constraints.
158    pub fn validate(&self, code: &str) -> Result<(), Vec<String>> {
159        let mut errors = Vec::new();
160
161        if let Some(ref constraints) = self.constraints {
162            // Check max lines
163            if let Some(max) = constraints.max_lines {
164                let lines = code.lines().count();
165                if lines > max {
166                    errors.push(format!("Code exceeds max lines: {} > {}", lines, max));
167                }
168            }
169
170            // Check max chars
171            if let Some(max) = constraints.max_chars {
172                if code.len() > max {
173                    errors.push(format!("Code exceeds max chars: {} > {}", code.len(), max));
174                }
175            }
176
177            // Check forbidden patterns
178            for pattern in &constraints.forbidden_patterns {
179                if let Ok(re) = regex::Regex::new(pattern) {
180                    if re.is_match(code) {
181                        errors.push(format!("Code contains forbidden pattern: {}", pattern));
182                    }
183                }
184            }
185        }
186
187        if errors.is_empty() {
188            Ok(())
189        } else {
190            Err(errors)
191        }
192    }
193}
194
195impl SlotConstraints {
196    /// Create new empty constraints.
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Set maximum lines.
202    pub fn max_lines(mut self, lines: usize) -> Self {
203        self.max_lines = Some(lines);
204        self
205    }
206
207    /// Set maximum characters.
208    pub fn max_chars(mut self, chars: usize) -> Self {
209        self.max_chars = Some(chars);
210        self
211    }
212
213    /// Set the target language.
214    pub fn language(mut self, lang: impl Into<String>) -> Self {
215        self.language = Some(lang.into());
216        self
217    }
218
219    /// Add a required import.
220    pub fn require_import(mut self, import: impl Into<String>) -> Self {
221        self.required_imports.push(import.into());
222        self
223    }
224
225    /// Add a forbidden pattern.
226    pub fn forbid_pattern(mut self, pattern: impl Into<String>) -> Self {
227        self.forbidden_patterns.push(pattern.into());
228        self
229    }
230
231    /// Set a TDD test harness.
232    pub fn test_harness(mut self, harness: impl Into<String>) -> Self {
233        self.test_harness = Some(harness.into());
234        self
235    }
236
237    /// Set a TDD test command.
238    pub fn test_command(mut self, command: impl Into<String>) -> Self {
239        self.test_command = Some(command.into());
240        self
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn test_slot_creation() {
250        let slot = Slot::new("test", "Generate a test");
251        assert_eq!(slot.name, "test");
252        assert_eq!(slot.prompt, "Generate a test");
253        assert!(slot.required);
254    }
255
256    #[test]
257    fn test_slot_validation() {
258        let slot = Slot::new("test", "")
259            .with_constraints(SlotConstraints::new().max_lines(5));
260
261        assert!(slot.validate("line1\nline2\nline3").is_ok());
262        assert!(slot.validate("1\n2\n3\n4\n5\n6").is_err());
263    }
264}