1use schemars::JsonSchema;
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34use std::path::Path;
35use thiserror::Error;
36
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
43pub struct GeneratorTemplate {
44 pub generator: GeneratorMeta,
46
47 #[serde(default)]
49 pub params: Vec<ParamSpec>,
50
51 pub template: TemplateSpec,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
57pub struct GeneratorMeta {
58 pub id: String,
60
61 pub name: String,
63
64 pub description: String,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub category: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
74pub struct ParamSpec {
75 pub name: String,
77
78 pub description: String,
80
81 #[serde(default)]
83 pub required: bool,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub default: Option<String>,
88
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub example: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96pub struct TemplateSpec {
97 pub code: String,
99
100 #[serde(default = "default_target_file")]
102 pub target_file: String,
103
104 #[serde(default)]
106 pub position: InsertPosition,
107}
108
109fn default_target_file() -> String {
110 "src/lib.rs".to_string()
111}
112
113#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
115#[serde(rename_all = "snake_case")]
116pub enum InsertPosition {
117 Top,
119 #[default]
121 Bottom,
122 After(String),
124 Before(String),
126}
127
128#[derive(Debug, Error)]
134pub enum GeneratorLoadError {
135 #[error("IO error: {0}")]
137 Io(#[from] std::io::Error),
138
139 #[error("YAML parse error: {0}")]
141 Yaml(#[from] serde_yaml::Error),
142
143 #[error("JSON parse error: {0}")]
145 Json(#[from] serde_json::Error),
146}
147
148#[derive(Debug, Clone, Error)]
150pub enum RenderError {
151 #[error("missing required parameters: {}", .0.join(", "))]
153 MissingParams(Vec<String>),
154}
155
156impl GeneratorTemplate {
161 pub fn id(&self) -> &str {
163 &self.generator.id
164 }
165
166 pub fn name(&self) -> &str {
168 &self.generator.name
169 }
170
171 pub fn description(&self) -> &str {
173 &self.generator.description
174 }
175
176 pub fn category(&self) -> Option<&str> {
178 self.generator.category.as_deref()
179 }
180
181 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 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 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 pub fn render(&self, params: &HashMap<String, String>) -> Result<String, RenderError> {
216 if let Err(missing) = self.validate_params(params) {
218 return Err(RenderError::MissingParams(missing));
219 }
220
221 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 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 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
252pub struct GeneratorLoader;
258
259impl GeneratorLoader {
260 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 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 Self::from_yaml(content).or_else(|_| Self::from_json(content))
281 }
282 }
283 }
284
285 pub fn from_yaml(yaml: &str) -> Result<GeneratorTemplate, GeneratorLoadError> {
287 Ok(serde_yaml::from_str(yaml)?)
288 }
289
290 pub fn from_json(json: &str) -> Result<GeneratorTemplate, GeneratorLoadError> {
292 Ok(serde_json::from_str(json)?)
293 }
294
295 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#[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(¶ms).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(); let result = template.render(¶ms);
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 let params = HashMap::new();
394 assert!(template.validate_params(¶ms).is_err());
395
396 let mut params = HashMap::new();
398 params.insert("name".to_string(), "Test".to_string());
399 assert!(template.validate_params(¶ms).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}