1use crate::error::{EngineError, EngineResult};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub struct TemplateConfig {
9    pub name: String,
10    pub description: String,
11    pub version: String,
12
13    #[serde(default)]
14    pub variables: Vec<TemplateVariable>,
15
16    #[serde(default)]
17    pub features: Vec<Feature>,
18
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub hooks: Option<Hooks>,
21
22    #[serde(default = "default_min_anvil_version")]
23    pub min_anvil_version: String,
24
25    #[serde(default)]
27    pub services: Vec<ServiceDefinition>,
28
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub composition: Option<CompositionConfig>,
31
32    #[serde(default)]
33    pub service_combinations: Vec<ServiceCombination>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TemplateVariable {
38    pub name: String,
39    #[serde(rename = "type")]
40    pub var_type: VariableType,
41    pub prompt: String,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub default: Option<serde_yaml::Value>,
44    #[serde(default)]
45    pub required: bool,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(tag = "type", rename_all = "snake_case")]
50pub enum VariableType {
51    String {
52        #[serde(default)]
53        min_length: usize,
54        #[serde(skip_serializing_if = "Option::is_none")]
55        max_length: Option<usize>,
56    },
57    Boolean,
58    Choice {
59        options: Vec<String>,
60    },
61    Number {
62        #[serde(skip_serializing_if = "Option::is_none")]
63        min: Option<i64>,
64        #[serde(skip_serializing_if = "Option::is_none")]
65        max: Option<i64>,
66    },
67}
68
69impl VariableType {
70    pub fn type_name(&self) -> String {
71        match self {
72            VariableType::String { .. } => "string".to_string(),
73            VariableType::Boolean => "boolean".to_string(),
74            VariableType::Choice { .. } => "choice".to_string(),
75            VariableType::Number { .. } => "number".to_string(),
76        }
77    }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Feature {
82    pub name: String,
83    pub description: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub enabled_when: Option<String>,
86    #[serde(default)]
87    pub dependencies: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Hooks {
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub pre_generate: Option<Vec<HookCommand>>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub post_generate: Option<Vec<HookCommand>>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct HookCommand {
100    pub command: String,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub working_dir: Option<String>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub condition: Option<String>,
105    #[serde(default)]
106    pub env: HashMap<String, String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub struct ServiceDefinition {
112    pub name: String,
113    pub category: ServiceCategory,
114    pub prompt: String,
115    pub options: Vec<String>,
116    #[serde(default)]
117    pub required: bool,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub default: Option<String>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub dependencies: Option<Vec<String>>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub conflicts: Option<Vec<String>>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub language_requirements: Option<Vec<String>>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub platform_requirements: Option<Vec<String>>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub compatibility_rules: Option<Vec<CompatibilityRule>>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
133#[serde(rename_all = "lowercase")]
134pub enum ServiceCategory {
135    Auth,
136    Payments,
137    Database,
138    #[serde(rename = "ai")]
139    AI,
140    Api,
141    Deployment,
142    Monitoring,
143    Email,
144    Storage,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct ServiceConfig {
149    pub name: String,
150    pub description: String,
151    pub version: String,
152    pub category: String,
153
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub dependencies: Option<ServiceDependencies>,
156
157    #[serde(default)]
158    pub environment_variables: Vec<EnvironmentVariable>,
159
160    #[serde(default)]
161    pub files: Vec<ServiceFile>,
162
163    #[serde(default)]
164    pub configuration_prompts: Vec<ServicePrompt>,
165
166    #[serde(default)]
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub language_requirements: Option<Vec<String>>,
169
170    #[serde(default)]
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub compatibility_rules: Option<Vec<CompatibilityRule>>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ServicePrompt {
177    pub name: String,
178    pub prompt: String,
179    pub prompt_type: ServicePromptType,
180    #[serde(default)]
181    pub required: bool,
182    #[serde(default)]
183    #[serde(skip_serializing_if = "Option::is_none")]
184    #[serde(deserialize_with = "deserialize_default_value")]
185    pub default: Option<Value>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub options: Option<Vec<String>>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub description: Option<String>,
190}
191
192fn deserialize_default_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
194where
195    D: serde::Deserializer<'de>,
196{
197    use serde::Deserialize;
198    Option::<Value>::deserialize(deserializer)
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum ServicePromptType {
204    Text,
205    Boolean,
206    Select,
207    MultiSelect,
208    Password,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ServiceDependencies {
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub npm: Option<Vec<String>>,
215
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub cargo: Option<HashMap<String, String>>,
218
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub go: Option<Vec<String>>,
221
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub python: Option<Vec<String>>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct EnvironmentVariable {
228    pub name: String,
229    pub description: String,
230    #[serde(default)]
231    pub required: bool,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub default: Option<String>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ServiceFile {
238    pub path: String,
239    pub description: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct CompositionConfig {
244    #[serde(default)]
245    pub file_merging_strategy: FileMergingStrategy,
246    #[serde(default)]
247    pub dependency_resolution: DependencyResolution,
248    #[serde(default)]
249    pub conditional_files: Vec<ConditionalFile>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum FileMergingStrategy {
255    Append,
256    Merge,
257    Override,
258    Skip,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "snake_case")]
263pub enum DependencyResolution {
264    Auto,
265    Manual,
266    Strict,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct ConditionalFile {
271    pub path: String,
272    pub condition: String,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub source_service: Option<String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CompatibilityRule {
279    pub rule_type: CompatibilityRuleType,
280    pub target_service: String,
281    pub condition: String,
282    pub message: String,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "snake_case")]
287pub enum CompatibilityRuleType {
288    Requires,
289    ConflictsWith,
290    RecommendsAgainst,
291    RequiresLanguage,
292    RequiresPlatform,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct ServiceCombination {
297    pub name: String,
298    pub description: String,
299    pub services: Vec<ServiceSpec>,
300    #[serde(default)]
301    pub recommended: bool,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub tags: Option<Vec<String>>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ServiceSpec {
308    pub category: ServiceCategory,
309    pub provider: String,
310    #[serde(default)]
311    pub config: std::collections::HashMap<String, serde_json::Value>,
312}
313
314impl TemplateConfig {
315    pub async fn from_file(path: &std::path::Path) -> EngineResult<Self> {
316        let content = tokio::fs::read_to_string(path)
317            .await
318            .map_err(|e| EngineError::file_error(path, e))?;
319
320        let config: TemplateConfig = serde_yaml::from_str(&content)?;
321        config.validate()?;
322        Ok(config)
323    }
324
325    pub fn validate(&self) -> EngineResult<()> {
326        if self.name.is_empty() {
327            return Err(EngineError::invalid_config("Template name cannot be empty"));
328        }
329
330        if self.description.is_empty() {
331            return Err(EngineError::invalid_config(
332                "Template description cannot be empty",
333            ));
334        }
335
336        semver::Version::parse(&self.version)
337            .map_err(|_| EngineError::invalid_config("Invalid version format"))?;
338
339        semver::Version::parse(&self.min_anvil_version)
340            .map_err(|_| EngineError::invalid_config("Invalid min_anvil_version format"))?;
341
342        for variable in &self.variables {
343            variable.validate()?;
344        }
345
346        for feature in &self.features {
347            feature.validate()?;
348        }
349
350        Ok(())
351    }
352
353    pub fn get_variable(&self, name: &str) -> Option<&TemplateVariable> {
354        self.variables.iter().find(|v| v.name == name)
355    }
356
357    pub fn get_feature(&self, name: &str) -> Option<&Feature> {
358        self.features.iter().find(|f| f.name == name)
359    }
360}
361
362impl TemplateVariable {
363    pub fn validate(&self) -> EngineResult<()> {
364        if self.name.is_empty() {
365            return Err(EngineError::invalid_config("Variable name cannot be empty"));
366        }
367
368        if self.prompt.is_empty() {
369            return Err(EngineError::invalid_config(format!(
370                "Variable '{}' must have a prompt",
371                self.name
372            )));
373        }
374
375        match &self.var_type {
376            VariableType::String {
377                min_length,
378                max_length,
379            } => {
380                if let Some(max) = max_length {
381                    if *min_length > *max {
382                        return Err(EngineError::invalid_config(format!(
383                            "Variable '{}': min_length cannot be greater than max_length",
384                            self.name
385                        )));
386                    }
387                }
388            }
389            VariableType::Choice { options } => {
390                if options.is_empty() {
391                    return Err(EngineError::invalid_config(format!(
392                        "Variable '{}': choice type must have at least one option",
393                        self.name
394                    )));
395                }
396            }
397            VariableType::Number { min, max } => {
398                if let (Some(min_val), Some(max_val)) = (min, max) {
399                    if min_val > max_val {
400                        return Err(EngineError::invalid_config(format!(
401                            "Variable '{}': min cannot be greater than max",
402                            self.name
403                        )));
404                    }
405                }
406            }
407            VariableType::Boolean => {}
408        }
409
410        Ok(())
411    }
412
413    pub fn validate_value(&self, value: &serde_yaml::Value) -> EngineResult<()> {
414        match (&self.var_type, value) {
415            (
416                VariableType::String {
417                    min_length,
418                    max_length,
419                },
420                serde_yaml::Value::String(s),
421            ) => {
422                if s.len() < *min_length {
423                    return Err(EngineError::variable_error(
424                        &self.name,
425                        format!("String too short (minimum {} characters)", min_length),
426                    ));
427                }
428                if let Some(max) = max_length {
429                    if s.len() > *max {
430                        return Err(EngineError::variable_error(
431                            &self.name,
432                            format!("String too long (maximum {} characters)", max),
433                        ));
434                    }
435                }
436            }
437            (VariableType::Boolean, serde_yaml::Value::Bool(_)) => {}
438            (VariableType::Number { min, max }, serde_yaml::Value::Number(n)) => {
439                if let Some(i) = n.as_i64() {
440                    if let Some(min_val) = min {
441                        if i < *min_val {
442                            return Err(EngineError::variable_error(
443                                &self.name,
444                                format!("Number too small (minimum {})", min_val),
445                            ));
446                        }
447                    }
448                    if let Some(max_val) = max {
449                        if i > *max_val {
450                            return Err(EngineError::variable_error(
451                                &self.name,
452                                format!("Number too large (maximum {})", max_val),
453                            ));
454                        }
455                    }
456                }
457            }
458            (VariableType::Choice { options }, serde_yaml::Value::String(s)) => {
459                if !options.contains(s) {
460                    return Err(EngineError::variable_error(
461                        &self.name,
462                        format!(
463                            "Invalid choice '{}'. Valid options: {}",
464                            s,
465                            options.join(", ")
466                        ),
467                    ));
468                }
469            }
470            _ => {
471                return Err(EngineError::variable_error(
472                    &self.name,
473                    format!("Value type mismatch for variable type {:?}", self.var_type),
474                ));
475            }
476        }
477        Ok(())
478    }
479}
480
481impl Feature {
482    pub fn validate(&self) -> EngineResult<()> {
483        if self.name.is_empty() {
484            return Err(EngineError::invalid_config("Feature name cannot be empty"));
485        }
486
487        if self.description.is_empty() {
488            return Err(EngineError::invalid_config(format!(
489                "Feature '{}' must have a description",
490                self.name
491            )));
492        }
493
494        Ok(())
495    }
496}
497
498fn default_min_anvil_version() -> String {
499    "0.1.0".to_string()
500}
501
502impl Default for FileMergingStrategy {
503    fn default() -> Self {
504        FileMergingStrategy::Merge
505    }
506}
507
508impl Default for DependencyResolution {
509    fn default() -> Self {
510        DependencyResolution::Auto
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::*;
517    use std::io::Write;
518    use tempfile::NamedTempFile;
519
520    #[tokio::test]
521    async fn test_valid_config_parsing() {
522        let yaml_content = r#"
523name: "test-template"
524description: "A test template"
525version: "1.0.0"
526variables:
527  - name: "project_name"
528    type:
529      type: "string"
530      min_length: 1
531    prompt: "Project name?"
532    required: true
533features:
534  - name: "database"
535    description: "Database integration"
536"#;
537
538        let mut temp_file = NamedTempFile::new().unwrap();
539        temp_file.write_all(yaml_content.as_bytes()).unwrap();
540
541        let config = TemplateConfig::from_file(temp_file.path()).await.unwrap();
542        assert_eq!(config.name, "test-template");
543        assert_eq!(config.variables.len(), 1);
544        assert_eq!(config.features.len(), 1);
545    }
546
547    #[test]
548    fn test_config_validation() {
549        let mut config = TemplateConfig {
550            name: "test".to_string(),
551            description: "Test template".to_string(),
552            version: "1.0.0".to_string(),
553            variables: vec![],
554            features: vec![],
555            hooks: None,
556            min_anvil_version: "0.1.0".to_string(),
557            services: vec![],
558            composition: None,
559            service_combinations: vec![],
560        };
561
562        assert!(config.validate().is_ok());
563
564        config.name = "".to_string();
565        assert!(config.validate().is_err());
566
567        config.name = "test".to_string();
568        config.version = "invalid-version".to_string();
569        assert!(config.validate().is_err());
570    }
571
572    #[test]
573    fn test_variable_validation() {
574        let variable = TemplateVariable {
575            name: "test_var".to_string(),
576            var_type: VariableType::String {
577                min_length: 1,
578                max_length: Some(10),
579            },
580            prompt: "Test variable?".to_string(),
581            default: None,
582            required: true,
583        };
584
585        assert!(variable.validate().is_ok());
586
587        assert!(
588            variable
589                .validate_value(&serde_yaml::Value::String("test".to_string()))
590                .is_ok()
591        );
592        assert!(
593            variable
594                .validate_value(&serde_yaml::Value::String("".to_string()))
595                .is_err()
596        );
597        assert!(
598            variable
599                .validate_value(&serde_yaml::Value::String("this_is_too_long".to_string()))
600                .is_err()
601        );
602    }
603}
604
605impl ServiceConfig {
606    pub async fn from_file(path: &std::path::Path) -> EngineResult<Self> {
607        let content = tokio::fs::read_to_string(path)
608            .await
609            .map_err(|e| EngineError::file_error(path, e))?;
610
611        let config: ServiceConfig = serde_yaml::from_str(&content)?;
612        Ok(config)
613    }
614}