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