Skip to main content

ryo_pattern/
generator.rs

1//! Generator Template Loading
2//!
3//! Loads generator templates from YAML configuration files.
4//! Generator templates define parameterized code generation patterns.
5//!
6//! # Example YAML
7//! ```yaml
8//! generator:
9//!   id: GEN001
10//!   name: domain_struct
11//!   description: Generate a domain struct with common derives
12//!   category: domain
13//!
14//! params:
15//!   - name: name
16//!     description: Struct name (e.g., Order)
17//!     required: true
18//!   - name: module
19//!     description: Target module path
20//!     required: false
21//!     default: src/lib.rs
22//!
23//! template:
24//!   code: |
25//!     #[derive(Debug, Clone, PartialEq, Eq)]
26//!     pub struct {{name}} {
27//!         pub id: String,
28//!     }
29//! ```
30
31use schemars::JsonSchema;
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34use std::path::Path;
35use thiserror::Error;
36
37// ============================================================================
38// Types
39// ============================================================================
40
41/// A generator template loaded from YAML
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
43pub struct GeneratorTemplate {
44    /// Generator metadata
45    pub generator: GeneratorMeta,
46
47    /// Parameter definitions
48    #[serde(default)]
49    pub params: Vec<ParamSpec>,
50
51    /// Code template
52    pub template: TemplateSpec,
53}
54
55/// Generator metadata
56#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
57pub struct GeneratorMeta {
58    /// Unique identifier (e.g., "GEN001")
59    pub id: String,
60
61    /// Human-readable name (used as suggest name)
62    pub name: String,
63
64    /// Description of what this generator does
65    pub description: String,
66
67    /// Category for grouping (e.g., "domain", "api", "test")
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub category: Option<String>,
70}
71
72/// Parameter specification
73#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
74pub struct ParamSpec {
75    /// Parameter name (used in template as {{name}})
76    pub name: String,
77
78    /// Description for LLM/user consumption
79    pub description: String,
80
81    /// Whether this parameter is required
82    #[serde(default)]
83    pub required: bool,
84
85    /// Default value if not provided
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub default: Option<String>,
88
89    /// Example value for documentation
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub example: Option<String>,
92}
93
94/// Code template specification
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96pub struct TemplateSpec {
97    /// The code template with {{param}} placeholders
98    pub code: String,
99
100    /// Target file path template (default: "src/lib.rs")
101    #[serde(default = "default_target_file")]
102    pub target_file: String,
103
104    /// Position hint for insertion
105    #[serde(default)]
106    pub position: InsertPosition,
107}
108
109fn default_target_file() -> String {
110    "src/lib.rs".to_string()
111}
112
113/// Where to insert the generated code
114#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
115#[serde(rename_all = "snake_case")]
116pub enum InsertPosition {
117    /// At the top of the file (after use statements)
118    Top,
119    /// At the bottom of the file
120    #[default]
121    Bottom,
122    /// After a specific pattern (e.g., "mod tests")
123    After(String),
124    /// Before a specific pattern
125    Before(String),
126}
127
128// ============================================================================
129// Errors
130// ============================================================================
131
132/// Errors from generator loading
133#[derive(Debug, Error)]
134pub enum GeneratorLoadError {
135    /// IO failure while reading generator file.
136    #[error("IO error: {0}")]
137    Io(#[from] std::io::Error),
138
139    /// YAML parse failure.
140    #[error("YAML parse error: {0}")]
141    Yaml(#[from] serde_yaml::Error),
142
143    /// JSON parse failure.
144    #[error("JSON parse error: {0}")]
145    Json(#[from] serde_json::Error),
146}
147
148/// Errors during template rendering
149#[derive(Debug, Clone, Error)]
150pub enum RenderError {
151    /// Required parameters are missing
152    #[error("missing required parameters: {}", .0.join(", "))]
153    MissingParams(Vec<String>),
154}
155
156// ============================================================================
157// GeneratorTemplate Implementation
158// ============================================================================
159
160impl GeneratorTemplate {
161    /// Get the generator ID
162    pub fn id(&self) -> &str {
163        &self.generator.id
164    }
165
166    /// Get the generator name
167    pub fn name(&self) -> &str {
168        &self.generator.name
169    }
170
171    /// Get the description
172    pub fn description(&self) -> &str {
173        &self.generator.description
174    }
175
176    /// Get category
177    pub fn category(&self) -> Option<&str> {
178        self.generator.category.as_deref()
179    }
180
181    /// Check if a parameter is required
182    pub fn is_param_required(&self, name: &str) -> bool {
183        self.params
184            .iter()
185            .find(|p| p.name == name)
186            .map(|p| p.required)
187            .unwrap_or(false)
188    }
189
190    /// Get default value for a parameter
191    pub fn get_param_default(&self, name: &str) -> Option<&str> {
192        self.params
193            .iter()
194            .find(|p| p.name == name)
195            .and_then(|p| p.default.as_deref())
196    }
197
198    /// Validate that all required parameters are provided
199    pub fn validate_params(&self, params: &HashMap<String, String>) -> Result<(), Vec<String>> {
200        let missing: Vec<String> = self
201            .params
202            .iter()
203            .filter(|p| p.required && !params.contains_key(&p.name) && p.default.is_none())
204            .map(|p| p.name.clone())
205            .collect();
206
207        if missing.is_empty() {
208            Ok(())
209        } else {
210            Err(missing)
211        }
212    }
213
214    /// Render the template with given parameters
215    pub fn render(&self, params: &HashMap<String, String>) -> Result<String, RenderError> {
216        // Validate required params
217        if let Err(missing) = self.validate_params(params) {
218            return Err(RenderError::MissingParams(missing));
219        }
220
221        // Build complete params with defaults
222        let mut complete_params = HashMap::new();
223        for spec in &self.params {
224            if let Some(value) = params.get(&spec.name) {
225                complete_params.insert(spec.name.clone(), value.clone());
226            } else if let Some(default) = &spec.default {
227                complete_params.insert(spec.name.clone(), default.clone());
228            }
229        }
230
231        // Simple template substitution ({{param}} -> value)
232        let mut result = self.template.code.clone();
233        for (key, value) in &complete_params {
234            let placeholder = format!("{{{{{}}}}}", key);
235            result = result.replace(&placeholder, value);
236        }
237
238        Ok(result)
239    }
240
241    /// Render target file path with parameters
242    pub fn render_target_file(&self, params: &HashMap<String, String>) -> String {
243        let mut result = self.template.target_file.clone();
244        for (key, value) in params {
245            let placeholder = format!("{{{{{}}}}}", key);
246            result = result.replace(&placeholder, value);
247        }
248        result
249    }
250}
251
252// ============================================================================
253// GeneratorLoader
254// ============================================================================
255
256/// Generator template loader
257pub struct GeneratorLoader;
258
259impl GeneratorLoader {
260    /// Load generator from a file (auto-detect format)
261    pub fn load_file(path: impl AsRef<Path>) -> Result<GeneratorTemplate, GeneratorLoadError> {
262        let path = path.as_ref();
263        let content = std::fs::read_to_string(path)?;
264        Self::load_from_str(&content, path)
265    }
266
267    /// Load generator from string with path hint for format detection
268    pub fn load_from_str(
269        content: &str,
270        path: impl AsRef<Path>,
271    ) -> Result<GeneratorTemplate, GeneratorLoadError> {
272        let path = path.as_ref();
273        let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
274
275        match ext {
276            "yaml" | "yml" => Self::from_yaml(content),
277            "json" => Self::from_json(content),
278            _ => {
279                // Try YAML first, then JSON
280                Self::from_yaml(content).or_else(|_| Self::from_json(content))
281            }
282        }
283    }
284
285    /// Load from YAML string
286    pub fn from_yaml(yaml: &str) -> Result<GeneratorTemplate, GeneratorLoadError> {
287        Ok(serde_yaml::from_str(yaml)?)
288    }
289
290    /// Load from JSON string
291    pub fn from_json(json: &str) -> Result<GeneratorTemplate, GeneratorLoadError> {
292        Ok(serde_json::from_str(json)?)
293    }
294
295    /// Load all generators from a directory
296    pub fn load_dir(dir: impl AsRef<Path>) -> Result<Vec<GeneratorTemplate>, GeneratorLoadError> {
297        let dir = dir.as_ref();
298        let mut templates = Vec::new();
299
300        if !dir.exists() {
301            return Ok(templates);
302        }
303
304        for entry in std::fs::read_dir(dir)? {
305            let entry = entry?;
306            let path = entry.path();
307
308            let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
309            if matches!(ext, "yaml" | "yml" | "json") {
310                match Self::load_file(&path) {
311                    Ok(template) => templates.push(template),
312                    Err(e) => {
313                        eprintln!(
314                            "Warning: Failed to load generator from {}: {}",
315                            path.display(),
316                            e
317                        );
318                    }
319                }
320            }
321        }
322
323        Ok(templates)
324    }
325}
326
327// ============================================================================
328// Tests
329// ============================================================================
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    const EXAMPLE_YAML: &str = r#"
336generator:
337  id: GEN001
338  name: domain_struct
339  description: Generate a domain struct
340  category: domain
341
342params:
343  - name: name
344    description: Struct name
345    required: true
346  - name: module
347    description: Target module
348    required: false
349    default: src/lib.rs
350
351template:
352  code: |
353    #[derive(Debug, Clone)]
354    pub struct {{name}} {
355        pub id: String,
356    }
357"#;
358
359    #[test]
360    fn test_parse_template() {
361        let template = GeneratorLoader::from_yaml(EXAMPLE_YAML).unwrap();
362        assert_eq!(template.id(), "GEN001");
363        assert_eq!(template.name(), "domain_struct");
364        assert_eq!(template.params.len(), 2);
365        assert!(template.is_param_required("name"));
366        assert!(!template.is_param_required("module"));
367    }
368
369    #[test]
370    fn test_render_template() {
371        let template = GeneratorLoader::from_yaml(EXAMPLE_YAML).unwrap();
372        let mut params = HashMap::new();
373        params.insert("name".to_string(), "Order".to_string());
374
375        let rendered = template.render(&params).unwrap();
376        assert!(rendered.contains("pub struct Order"));
377    }
378
379    #[test]
380    fn test_missing_required_param() {
381        let template = GeneratorLoader::from_yaml(EXAMPLE_YAML).unwrap();
382        let params = HashMap::new(); // Missing "name"
383
384        let result = template.render(&params);
385        assert!(result.is_err());
386    }
387
388    #[test]
389    fn test_validate_params() {
390        let template = GeneratorLoader::from_yaml(EXAMPLE_YAML).unwrap();
391
392        // Missing required param
393        let params = HashMap::new();
394        assert!(template.validate_params(&params).is_err());
395
396        // All required params present
397        let mut params = HashMap::new();
398        params.insert("name".to_string(), "Test".to_string());
399        assert!(template.validate_params(&params).is_ok());
400    }
401
402    #[test]
403    fn test_json_format() {
404        let json = r#"{
405            "generator": {
406                "id": "GEN002",
407                "name": "api_endpoint",
408                "description": "Generate API endpoint"
409            },
410            "params": [
411                {"name": "resource", "description": "Resource name", "required": true}
412            ],
413            "template": {
414                "code": "pub fn get_{{resource}}() {}"
415            }
416        }"#;
417
418        let template = GeneratorLoader::from_json(json).unwrap();
419        assert_eq!(template.id(), "GEN002");
420    }
421}