1use serde::Serialize;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use tokio::fs;
11
12use crate::config::{CompositionConfig, FileMergingStrategy, ServiceCategory, TemplateConfig};
13use crate::error::{EngineError, EngineResult};
14
15#[derive(Debug, Clone)]
16pub struct CompositionEngine {
17    base_template_path: PathBuf,
18    shared_services_path: PathBuf,
19}
20
21#[derive(Debug, Clone)]
22pub struct ServiceSelection {
23    pub category: ServiceCategory,
24    pub provider: String,
25    pub config: HashMap<String, Value>,
26}
27
28#[derive(Debug, Clone)]
29pub struct ComposedTemplate {
30    pub base_config: TemplateConfig,
31    pub files: Vec<ComposedFile>,
32    pub merged_dependencies: HashMap<String, Value>,
33    pub environment_variables: HashMap<String, String>,
34    pub service_context: ServiceContext,
35}
36
37#[derive(Debug, Clone)]
38pub struct ComposedFile {
39    pub path: PathBuf,
40    pub content: String,
41    pub source: FileSource,
42    pub merge_strategy: FileMergingStrategy,
43    pub is_template: bool,
44}
45
46#[derive(Debug, Clone)]
47pub enum FileSource {
48    BaseTemplate,
49    Service {
50        category: ServiceCategory,
51        provider: String,
52    },
53    Merged,
54}
55
56#[derive(Debug, Clone, Serialize)]
57pub struct ServiceContext {
58    pub services: HashMap<String, ServiceInfo>,
59    pub shared_config: HashMap<String, Value>,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct ServiceInfo {
64    pub provider: String,
65    pub config: HashMap<String, Value>,
66    pub exports: HashMap<String, Value>,
67}
68
69impl CompositionEngine {
70    pub fn new(base_template_path: PathBuf, shared_services_path: PathBuf) -> Self {
71        Self {
72            base_template_path,
73            shared_services_path,
74        }
75    }
76
77    pub fn shared_services_path(&self) -> &PathBuf {
78        &self.shared_services_path
79    }
80
81    pub async fn discover_service_providers(
86        &self,
87        category: ServiceCategory,
88    ) -> EngineResult<Vec<String>> {
89        let category_dir = self
90            .shared_services_path
91            .join(format!("{:?}", category).to_lowercase());
92
93        if !category_dir.exists() {
94            return Ok(vec!["none".to_string()]);
95        }
96
97        let mut providers = vec!["none".to_string()];
98        let mut entries = fs::read_dir(&category_dir)
99            .await
100            .map_err(|e| EngineError::file_error(&category_dir, e))?;
101
102        while let Some(entry) = entries
103            .next_entry()
104            .await
105            .map_err(|e| EngineError::file_error(&category_dir, e))?
106        {
107            let path = entry.path();
108            if path.is_dir() {
109                if let Some(provider_name) = path.file_name().and_then(|n| n.to_str()) {
110                    let config_path = path.join("anvil.yaml");
112                    if config_path.exists() {
113                        providers.push(provider_name.to_string());
114                    }
115                }
116            }
117        }
118
119        providers.sort();
120        Ok(providers)
121    }
122
123    pub async fn discover_all_services(
128        &self,
129    ) -> EngineResult<HashMap<ServiceCategory, Vec<String>>> {
130        let mut all_services = HashMap::new();
131
132        for category in [
134            ServiceCategory::Auth,
135            ServiceCategory::Payments,
136            ServiceCategory::Database,
137            ServiceCategory::AI,
138            ServiceCategory::Api,
139            ServiceCategory::Deployment,
140            ServiceCategory::Monitoring,
141            ServiceCategory::Email,
142            ServiceCategory::Storage,
143        ] {
144            let providers = self.discover_service_providers(category.clone()).await?;
145            all_services.insert(category, providers);
146        }
147
148        Ok(all_services)
149    }
150
151    pub async fn compose_template(
156        &self,
157        template_name: &str,
158        services: Vec<ServiceSelection>,
159    ) -> EngineResult<ComposedTemplate> {
160        self.compose_template_with_context(template_name, services, None)
161            .await
162    }
163
164    pub async fn compose_template_with_context(
169        &self,
170        template_name: &str,
171        services: Vec<ServiceSelection>,
172        context: Option<&crate::Context>,
173    ) -> EngineResult<ComposedTemplate> {
174        let base_config_path = self
176            .base_template_path
177            .join(template_name)
178            .join("anvil.yaml");
179        let base_config = TemplateConfig::from_file(&base_config_path).await?;
180
181        self.validate_service_selections(&base_config, &services, context)
183            .await?;
184
185        let service_context = self.build_service_context(&services).await?;
187
188        let mut composed_files = self.collect_base_template_files(template_name).await?;
190
191        for service in &services {
193            let service_files = self.collect_service_files(&service).await?;
194            composed_files.extend(service_files);
195        }
196
197        let filtered_files = self
199            .apply_conditional_inclusion(composed_files, &services, &base_config.composition)
200            .await?;
201
202        let resolved_files = self
204            .resolve_file_conflicts(filtered_files, &base_config.composition)
205            .await?;
206
207        let merged_dependencies = self.merge_dependencies(&services).await?;
209
210        let environment_variables = self.collect_environment_variables(&services).await?;
212
213        Ok(ComposedTemplate {
214            base_config,
215            files: resolved_files,
216            merged_dependencies,
217            environment_variables,
218            service_context,
219        })
220    }
221
222    async fn validate_service_selections(
227        &self,
228        base_config: &TemplateConfig,
229        services: &[ServiceSelection],
230        context: Option<&crate::Context>,
231    ) -> EngineResult<()> {
232        for service_def in &base_config.services {
234            if service_def.required {
235                let has_service = services.iter().any(|s| s.category == service_def.category);
236                if !has_service {
237                    return Err(EngineError::composition_error(format!(
238                        "Required service '{}' not provided",
239                        service_def.name
240                    )));
241                }
242            }
243        }
244
245        for service in services {
247            let service_def = base_config
249                .services
250                .iter()
251                .find(|s| s.category == service.category)
252                .ok_or_else(|| {
253                    EngineError::composition_error(format!(
254                        "Service category '{:?}' not supported by template '{}'",
255                        service.category, base_config.name
256                    ))
257                })?;
258
259            if !service_def.options.contains(&service.provider) {
261                return Err(EngineError::composition_error(format!(
262                    "Invalid provider '{}' for service '{:?}'. Valid options: {:?}",
263                    service.provider, service.category, service_def.options
264                )));
265            }
266
267            let service_path = self
269                .shared_services_path
270                .join(format!("{:?}", service.category).to_lowercase())
271                .join(&service.provider);
272
273            if !service_path.exists() {
274                return Err(EngineError::composition_error(format!(
275                    "Service files not found for '{:?}/{}'",
276                    service.category, service.provider
277                )));
278            }
279        }
280
281        for service in services {
283            if let Some(service_def) = base_config
284                .services
285                .iter()
286                .find(|s| s.category == service.category)
287            {
288                if let Some(conflicts) = &service_def.conflicts {
289                    for conflict in conflicts {
290                        if services
291                            .iter()
292                            .any(|s| format!("{:?}", s.category).to_lowercase() == *conflict)
293                        {
294                            return Err(EngineError::composition_error(format!(
295                                "Service conflict: {} conflicts with {}",
296                                service_def.name, conflict
297                            )));
298                        }
299                    }
300                }
301            }
302        }
303
304        for service in services {
306            if let Some(service_def) = base_config
307                .services
308                .iter()
309                .find(|s| s.category == service.category)
310            {
311                if let Some(dependencies) = &service_def.dependencies {
312                    for dependency in dependencies {
313                        let has_dependency = services
314                            .iter()
315                            .any(|s| format!("{:?}", s.category).to_lowercase() == *dependency);
316
317                        if !has_dependency {
318                            return Err(EngineError::composition_error(format!(
319                                "Service '{}' requires dependency '{}' which is not selected",
320                                service_def.name, dependency
321                            )));
322                        }
323                    }
324                }
325            }
326        }
327
328        self.validate_service_compatibility(base_config, services, context)
330            .await?;
331
332        Ok(())
333    }
334
335    async fn validate_service_compatibility(
340        &self,
341        base_config: &TemplateConfig,
342        services: &[ServiceSelection],
343        context: Option<&crate::Context>,
344    ) -> EngineResult<()> {
345        let project_language = self.detect_project_language(base_config, context).await?;
347
348        for service in services {
350            let service_config_path = self
351                .shared_services_path
352                .join(format!("{:?}", service.category).to_lowercase())
353                .join(&service.provider)
354                .join("anvil.yaml");
355
356            if service_config_path.exists() {
357                let service_config =
358                    crate::config::ServiceConfig::from_file(&service_config_path).await?;
359
360                if let Some(language_reqs) = self
362                    .get_service_language_requirements(&service_config)
363                    .await?
364                {
365                    for required_lang in &language_reqs {
366                        if !project_language.contains(required_lang) {
367                            return Err(EngineError::composition_error(format!(
368                                "Service '{}/{}' requires {} but project language is {:?}",
369                                format!("{:?}", service.category).to_lowercase(),
370                                service.provider,
371                                required_lang,
372                                project_language
373                            )));
374                        }
375                    }
376                }
377
378                self.validate_service_rules(&service_config, services, &project_language)
380                    .await?;
381            }
382        }
383
384        self.validate_cross_service_compatibility(services).await?;
386
387        Ok(())
388    }
389
390    async fn detect_project_language(
394        &self,
395        base_config: &TemplateConfig,
396        context: Option<&crate::Context>,
397    ) -> EngineResult<Vec<String>> {
398        let mut languages = Vec::new();
399
400        if let Some(ctx) = context {
402            if let Some(lang_value) = ctx.get_variable("language") {
403                if let Some(lang_str) = lang_value.as_str() {
404                    languages.push(lang_str.to_string());
405                    return Ok(languages);
406                }
407            }
408        }
409
410        if base_config.name.contains("rust") {
412            languages.push("rust".to_string());
413        } else if base_config.name.contains("go") {
414            languages.push("go".to_string());
415        } else if base_config.name.contains("python") {
416            languages.push("python".to_string());
417        } else {
418            languages.push("typescript".to_string());
420            languages.push("javascript".to_string());
421        }
422
423        Ok(languages)
427    }
428
429    async fn get_service_language_requirements(
433        &self,
434        service_config: &crate::config::ServiceConfig,
435    ) -> EngineResult<Option<Vec<String>>> {
436        Ok(service_config.language_requirements.clone())
437    }
438
439    async fn validate_service_rules(
443        &self,
444        service_config: &crate::config::ServiceConfig,
445        _services: &[ServiceSelection],
446        project_languages: &[String],
447    ) -> EngineResult<()> {
448        match service_config.name.as_str() {
453            "trpc-api" => {
454                if !project_languages.contains(&"typescript".to_string()) {
455                    return Err(EngineError::composition_error(
456                        "tRPC requires TypeScript for type safety".to_string(),
457                    ));
458                }
459            }
460            _ => {}
461        }
462
463        Ok(())
464    }
465
466    async fn validate_cross_service_compatibility(
470        &self,
471        services: &[ServiceSelection],
472    ) -> EngineResult<()> {
473        let _service_providers: std::collections::HashSet<String> = services
475            .iter()
476            .map(|s| format!("{:?}/{}", s.category, s.provider))
477            .collect();
478
479        let auth_services: Vec<_> = services
481            .iter()
482            .filter(|s| s.category == ServiceCategory::Auth)
483            .collect();
484
485        if auth_services.len() > 1 {
486            let auth_providers: Vec<_> =
487                auth_services.iter().map(|s| s.provider.as_str()).collect();
488            return Err(EngineError::composition_error(format!(
489                "Multiple auth providers selected: {:?}. Only one auth provider is allowed.",
490                auth_providers
491            )));
492        }
493
494        let api_services: Vec<_> = services
496            .iter()
497            .filter(|s| s.category == ServiceCategory::Api)
498            .collect();
499
500        if api_services.len() > 1 {
501            let api_providers: Vec<_> = api_services.iter().map(|s| s.provider.as_str()).collect();
502            return Err(EngineError::composition_error(format!(
503                "Multiple API patterns selected: {:?}. Only one API pattern is recommended.",
504                api_providers
505            )));
506        }
507
508        Ok(())
511    }
512
513    async fn build_service_context(
518        &self,
519        services: &[ServiceSelection],
520    ) -> EngineResult<ServiceContext> {
521        let mut service_context = ServiceContext {
522            services: HashMap::new(),
523            shared_config: HashMap::new(),
524        };
525
526        for service in services {
528            if service.provider == "none" {
530                continue;
531            }
532
533            let service_config_path = self
534                .shared_services_path
535                .join(format!("{:?}", service.category).to_lowercase())
536                .join(&service.provider)
537                .join("anvil.yaml");
538
539            if service_config_path.exists() {
540                let service_config =
541                    crate::config::ServiceConfig::from_file(&service_config_path).await?;
542
543                let mut exports = HashMap::new();
545
546                exports.insert(
548                    "provider".to_string(),
549                    Value::String(service.provider.clone()),
550                );
551                exports.insert(
552                    "category".to_string(),
553                    Value::String(format!("{:?}", service.category)),
554                );
555
556                match service.category {
558                    ServiceCategory::Auth => {
559                        exports.insert(
560                            "auth_provider".to_string(),
561                            Value::String(service.provider.clone()),
562                        );
563                        exports.insert("has_auth".to_string(), Value::Bool(true));
564                        for env_var in &service_config.environment_variables {
566                            if env_var.name.contains("PUBLISHABLE")
567                                || env_var.name.contains("PUBLIC")
568                            {
569                                exports.insert(
570                                    "public_auth_key_name".to_string(),
571                                    Value::String(env_var.name.clone()),
572                                );
573                            }
574                        }
575                    }
576                    ServiceCategory::Database => {
577                        exports.insert(
578                            "database_provider".to_string(),
579                            Value::String(service.provider.clone()),
580                        );
581                        exports.insert("has_database".to_string(), Value::Bool(true));
582                    }
583                    ServiceCategory::Payments => {
584                        exports.insert(
585                            "payments_provider".to_string(),
586                            Value::String(service.provider.clone()),
587                        );
588                        exports.insert("has_payments".to_string(), Value::Bool(true));
589                    }
590                    ServiceCategory::AI => {
591                        exports.insert(
592                            "ai_provider".to_string(),
593                            Value::String(service.provider.clone()),
594                        );
595                        exports.insert("has_ai".to_string(), Value::Bool(true));
596                    }
597                    ServiceCategory::Api => {
598                        exports.insert(
599                            "api_pattern".to_string(),
600                            Value::String(service.provider.clone()),
601                        );
602                        exports.insert("has_api".to_string(), Value::Bool(true));
603                        exports.insert(
604                            "api_type".to_string(),
605                            Value::String(service.provider.clone()),
606                        );
607                    }
608                    _ => {
609                        exports.insert(
611                            format!("has_{}", format!("{:?}", service.category).to_lowercase()),
612                            Value::Bool(true),
613                        );
614                    }
615                }
616
617                let mut enriched_config = service.config.clone();
619                for prompt in &service_config.configuration_prompts {
620                    if !enriched_config.contains_key(&prompt.name) {
621                        if let Some(default_val) = &prompt.default {
622                            enriched_config.insert(prompt.name.clone(), default_val.clone());
623                        }
624                    }
625                }
626
627                let mut all_exports = exports;
629                for (key, value) in &enriched_config {
630                    let category_name = format!("{:?}", service.category).to_lowercase();
632                    all_exports.insert(format!("{}_config_{}", category_name, key), value.clone());
633                }
634
635                let service_info = ServiceInfo {
636                    provider: service.provider.clone(),
637                    config: service.config.clone(),
638                    exports: all_exports,
639                };
640
641                service_context
642                    .services
643                    .insert(format!("{:?}", service.category), service_info);
644            }
645        }
646
647        let mut has_any_auth = false;
649        let mut has_any_database = false;
650
651        for (category_str, _service_info) in &service_context.services {
652            match category_str.as_str() {
653                "Auth" => has_any_auth = true,
654                "Database" => has_any_database = true,
655                _ => {}
656            }
657        }
658
659        service_context
660            .shared_config
661            .insert("has_any_auth".to_string(), Value::Bool(has_any_auth));
662        service_context.shared_config.insert(
663            "has_any_database".to_string(),
664            Value::Bool(has_any_database),
665        );
666        service_context.shared_config.insert(
667            "service_count".to_string(),
668            Value::Number(serde_json::Number::from(services.len())),
669        );
670
671        Ok(service_context)
672    }
673
674    async fn collect_base_template_files(
678        &self,
679        template_name: &str,
680    ) -> EngineResult<Vec<ComposedFile>> {
681        let template_path = self.base_template_path.join(template_name);
682        let mut files = Vec::new();
683
684        self.collect_files_recursive(
685            &template_path,
686            &template_path,
687            FileSource::BaseTemplate,
688            &mut files,
689        )
690        .await?;
691
692        Ok(files)
693    }
694
695    async fn collect_service_files(
699        &self,
700        service: &ServiceSelection,
701    ) -> EngineResult<Vec<ComposedFile>> {
702        if service.provider == "none" {
704            return Ok(Vec::new());
705        }
706
707        let service_path = self
708            .shared_services_path
709            .join(format!("{:?}", service.category).to_lowercase())
710            .join(&service.provider);
711
712        if !service_path.exists() {
713            return Err(EngineError::composition_error(format!(
714                "Service files not found: {:?}/{}",
715                service.category, service.provider
716            )));
717        }
718
719        let mut files = Vec::new();
720        let source = FileSource::Service {
721            category: service.category.clone(),
722            provider: service.provider.clone(),
723        };
724
725        self.collect_files_recursive(&service_path, &service_path, source, &mut files)
726            .await?;
727
728        Ok(files)
729    }
730
731    fn collect_files_recursive<'a>(
735        &'a self,
736        dir: &'a Path,
737        base_path: &'a Path,
738        source: FileSource,
739        files: &'a mut Vec<ComposedFile>,
740    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = EngineResult<()>> + Send + 'a>> {
741        Box::pin(async move {
742            let mut entries = fs::read_dir(dir)
743                .await
744                .map_err(|e| EngineError::file_error(dir, e))?;
745
746            while let Some(entry) = entries
747                .next_entry()
748                .await
749                .map_err(|e| EngineError::file_error(dir, e))?
750            {
751                let path = entry.path();
752
753                if path.is_dir() {
754                    self.collect_files_recursive(&path, base_path, source.clone(), files)
755                        .await?;
756                } else if path.is_file() {
757                    if path.file_name().and_then(|name| name.to_str()) == Some("anvil.yaml") {
759                        continue;
760                    }
761
762                    let relative_path = path.strip_prefix(base_path).map_err(|_| {
763                        EngineError::composition_error(format!(
764                            "Invalid path structure: {}",
765                            path.display()
766                        ))
767                    })?;
768
769                    let content = fs::read_to_string(&path)
770                        .await
771                        .map_err(|e| EngineError::file_error(&path, e))?;
772
773                    let is_template =
775                        relative_path.extension().and_then(|e| e.to_str()) == Some("tera");
776
777                    let output_path = if is_template {
779                        let file_name = relative_path
781                            .file_name()
782                            .and_then(|n| n.to_str())
783                            .unwrap_or("")
784                            .trim_end_matches(".tera");
785                        relative_path.with_file_name(file_name)
786                    } else {
787                        relative_path.to_path_buf()
788                    };
789
790                    files.push(ComposedFile {
791                        path: output_path,
792                        content,
793                        source: source.clone(),
794                        merge_strategy: FileMergingStrategy::default(),
795                        is_template,
796                    });
797                }
798            }
799
800            Ok(())
801        })
802    }
803
804    async fn resolve_file_conflicts(
809        &self,
810        files: Vec<ComposedFile>,
811        composition_config: &Option<CompositionConfig>,
812    ) -> EngineResult<Vec<ComposedFile>> {
813        let mut file_map: HashMap<PathBuf, Vec<ComposedFile>> = HashMap::new();
814
815        for file in files {
817            file_map
818                .entry(file.path.clone())
819                .or_insert_with(Vec::new)
820                .push(file);
821        }
822
823        let mut resolved_files = Vec::new();
824
825        for (path, conflicting_files) in file_map {
826            if conflicting_files.len() == 1 {
827                resolved_files.push(conflicting_files.into_iter().next().unwrap());
829            } else {
830                let default_strategy = FileMergingStrategy::default();
832                let strategy = composition_config
833                    .as_ref()
834                    .map(|c| &c.file_merging_strategy)
835                    .unwrap_or(&default_strategy);
836
837                let resolved = self
838                    .resolve_single_conflict(path, conflicting_files, strategy)
839                    .await?;
840                resolved_files.push(resolved);
841            }
842        }
843
844        Ok(resolved_files)
845    }
846
847    async fn resolve_single_conflict(
851        &self,
852        path: PathBuf,
853        mut files: Vec<ComposedFile>,
854        strategy: &FileMergingStrategy,
855    ) -> EngineResult<ComposedFile> {
856        match strategy {
857            FileMergingStrategy::Override => {
858                files.sort_by(|a, b| match (&a.source, &b.source) {
860                    (FileSource::BaseTemplate, FileSource::Service { .. }) => {
861                        std::cmp::Ordering::Less
862                    }
863                    (FileSource::Service { .. }, FileSource::BaseTemplate) => {
864                        std::cmp::Ordering::Greater
865                    }
866                    _ => std::cmp::Ordering::Equal,
867                });
868                Ok(files.into_iter().last().unwrap())
869            }
870            FileMergingStrategy::Append => {
871                let mut combined_content = String::new();
873                for file in &files {
874                    combined_content.push_str(&file.content);
875                    combined_content.push('\n');
876                }
877                Ok(ComposedFile {
878                    path,
879                    content: combined_content,
880                    source: FileSource::Merged,
881                    merge_strategy: FileMergingStrategy::Append,
882                    is_template: false,
883                })
884            }
885            FileMergingStrategy::Merge => {
886                if path.extension().and_then(|ext| ext.to_str()) == Some("json") {
888                    self.merge_json_files(path, files).await
889                } else {
890                    let mut combined_content = String::new();
892                    for file in &files {
893                        combined_content.push_str(&file.content);
894                        combined_content.push('\n');
895                    }
896                    Ok(ComposedFile {
897                        path,
898                        content: combined_content,
899                        source: FileSource::Merged,
900                        merge_strategy: FileMergingStrategy::Append,
901                        is_template: false,
902                    })
903                }
904            }
905            FileMergingStrategy::Skip => {
906                Ok(files.into_iter().next().unwrap())
908            }
909        }
910    }
911
912    async fn merge_json_files(
917        &self,
918        path: PathBuf,
919        files: Vec<ComposedFile>,
920    ) -> EngineResult<ComposedFile> {
921        let mut merged_json = serde_json::Map::new();
922
923        for file in &files {
924            let json: serde_json::Value = serde_json::from_str(&file.content).map_err(|e| {
925                EngineError::composition_error(format!("Invalid JSON in {}: {}", path.display(), e))
926            })?;
927
928            if let serde_json::Value::Object(obj) = json {
929                for (key, value) in obj {
930                    match merged_json.get(&key) {
931                        Some(existing) => {
932                            if (key == "dependencies" || key == "devDependencies")
934                                && existing.is_object()
935                                && value.is_object()
936                            {
937                                let mut merged_deps = existing.as_object().unwrap().clone();
938                                merged_deps.extend(value.as_object().unwrap().clone());
939                                merged_json.insert(key, serde_json::Value::Object(merged_deps));
940                            } else {
941                                merged_json.insert(key, value);
943                            }
944                        }
945                        None => {
946                            merged_json.insert(key, value);
947                        }
948                    }
949                }
950            }
951        }
952
953        let merged_content = serde_json::to_string_pretty(&merged_json).map_err(|e| {
954            EngineError::composition_error(format!("Failed to serialize merged JSON: {}", e))
955        })?;
956
957        Ok(ComposedFile {
958            path,
959            content: merged_content,
960            source: FileSource::Merged,
961            merge_strategy: FileMergingStrategy::Merge,
962            is_template: false,
963        })
964    }
965
966    async fn merge_dependencies(
971        &self,
972        services: &[ServiceSelection],
973    ) -> EngineResult<HashMap<String, Value>> {
974        let mut merged_deps = HashMap::new();
975        let mut npm_dependencies = Vec::new();
976        let mut cargo_dependencies = HashMap::new();
977        let mut environment_variables = Vec::new();
978
979        for service in services {
981            let service_config_path = self
982                .shared_services_path
983                .join(format!("{:?}", service.category).to_lowercase())
984                .join(&service.provider)
985                .join("anvil.yaml");
986
987            if service_config_path.exists() {
988                let service_config =
989                    crate::config::ServiceConfig::from_file(&service_config_path).await?;
990
991                if let Some(deps) = &service_config.dependencies {
993                    if let Some(npm_deps) = &deps.npm {
994                        npm_dependencies.extend(npm_deps.iter().cloned());
995                    }
996
997                    if let Some(cargo_deps) = &deps.cargo {
998                        cargo_dependencies
999                            .extend(cargo_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
1000                    }
1001                }
1002
1003                environment_variables.extend(service_config.environment_variables);
1005            }
1006        }
1007
1008        if !npm_dependencies.is_empty() {
1010            let npm_deps: Vec<Value> = npm_dependencies
1011                .into_iter()
1012                .map(|dep| {
1013                    if let Some(_at_pos) = dep.rfind('@') {
1015                        if dep.starts_with('@') && dep[1..].contains('@') {
1017                            let parts: Vec<&str> = dep.rsplitn(2, '@').collect();
1019                            if parts.len() == 2 {
1020                                let mut dep_obj = serde_json::Map::new();
1021                                dep_obj.insert(
1022                                    "name".to_string(),
1023                                    Value::String(parts[1].to_string()),
1024                                );
1025                                dep_obj.insert(
1026                                    "version".to_string(),
1027                                    Value::String(parts[0].to_string()),
1028                                );
1029                                return Value::Object(dep_obj);
1030                            }
1031                        } else if !dep.starts_with('@') {
1032                            let parts: Vec<&str> = dep.splitn(2, '@').collect();
1034                            if parts.len() == 2 {
1035                                let mut dep_obj = serde_json::Map::new();
1036                                dep_obj.insert(
1037                                    "name".to_string(),
1038                                    Value::String(parts[0].to_string()),
1039                                );
1040                                dep_obj.insert(
1041                                    "version".to_string(),
1042                                    Value::String(parts[1].to_string()),
1043                                );
1044                                return Value::Object(dep_obj);
1045                            }
1046                        }
1047                    }
1048
1049                    let mut dep_obj = serde_json::Map::new();
1051                    dep_obj.insert("name".to_string(), Value::String(dep));
1052                    dep_obj.insert("version".to_string(), Value::String("^1.0.0".to_string()));
1053                    Value::Object(dep_obj)
1054                })
1055                .collect();
1056
1057            merged_deps.insert("npm".to_string(), Value::Array(npm_deps));
1058        }
1059
1060        if !cargo_dependencies.is_empty() {
1061            let cargo_map: serde_json::Map<String, Value> = cargo_dependencies
1062                .into_iter()
1063                .map(|(k, v)| (k, Value::String(v)))
1064                .collect();
1065            merged_deps.insert("cargo".to_string(), Value::Object(cargo_map));
1066        }
1067
1068        if !environment_variables.is_empty() {
1069            let env_array: Vec<Value> = environment_variables
1070                .into_iter()
1071                .map(|env_var| {
1072                    let mut env_map = serde_json::Map::new();
1073                    env_map.insert("name".to_string(), Value::String(env_var.name));
1074                    env_map.insert(
1075                        "description".to_string(),
1076                        Value::String(env_var.description),
1077                    );
1078                    env_map.insert("required".to_string(), Value::Bool(env_var.required));
1079                    if let Some(default) = env_var.default {
1080                        env_map.insert("default".to_string(), Value::String(default));
1081                    }
1082                    Value::Object(env_map)
1083                })
1084                .collect();
1085
1086            merged_deps.insert("environment_variables".to_string(), Value::Array(env_array));
1087        }
1088
1089        Ok(merged_deps)
1090    }
1091
1092    async fn apply_conditional_inclusion(
1097        &self,
1098        files: Vec<ComposedFile>,
1099        services: &[ServiceSelection],
1100        composition_config: &Option<CompositionConfig>,
1101    ) -> EngineResult<Vec<ComposedFile>> {
1102        let mut filtered_files = Vec::new();
1103
1104        let mut context = HashMap::new();
1106
1107        for service in services {
1109            let category_key = format!("{:?}", service.category).to_lowercase();
1110            context.insert(category_key, service.provider.clone());
1111            context.insert(
1112                format!("has_{}", format!("{:?}", service.category).to_lowercase()),
1113                "true".to_string(),
1114            );
1115        }
1116
1117        for file in files {
1118            let should_include = self
1119                .evaluate_file_conditions(&file, &context, composition_config)
1120                .await?;
1121
1122            if should_include {
1123                filtered_files.push(file);
1124            }
1125        }
1126
1127        Ok(filtered_files)
1128    }
1129
1130    async fn evaluate_file_conditions(
1134        &self,
1135        file: &ComposedFile,
1136        context: &HashMap<String, String>,
1137        composition_config: &Option<CompositionConfig>,
1138    ) -> EngineResult<bool> {
1139        if let Some(config) = composition_config {
1141            for conditional_file in &config.conditional_files {
1142                if file.path == PathBuf::from(&conditional_file.path) {
1143                    return self
1144                        .evaluate_condition(&conditional_file.condition, context)
1145                        .await;
1146                }
1147            }
1148        }
1149
1150        self.evaluate_implicit_conditions(file, context).await
1152    }
1153
1154    async fn evaluate_condition(
1162        &self,
1163        condition: &str,
1164        context: &HashMap<String, String>,
1165    ) -> EngineResult<bool> {
1166        let condition = condition.trim();
1167
1168        if condition.contains("&&") {
1170            let parts: Vec<&str> = condition.split("&&").collect();
1171            for part in parts {
1172                let result = self.evaluate_simple_condition(part.trim(), context)?;
1173                if !result {
1174                    return Ok(false);
1175                }
1176            }
1177            return Ok(true);
1178        }
1179
1180        if condition.contains("||") {
1182            let parts: Vec<&str> = condition.split("||").collect();
1183            for part in parts {
1184                let result = self.evaluate_simple_condition(part.trim(), context)?;
1185                if result {
1186                    return Ok(true);
1187                }
1188            }
1189            return Ok(false);
1190        }
1191
1192        self.evaluate_simple_condition(condition, context)
1194    }
1195
1196    fn evaluate_simple_condition(
1201        &self,
1202        condition: &str,
1203        context: &HashMap<String, String>,
1204    ) -> EngineResult<bool> {
1205        let condition = condition.trim();
1206
1207        if condition.contains("==") {
1209            let parts: Vec<&str> = condition.split("==").collect();
1210            if parts.len() == 2 {
1211                let left = parts[0].trim();
1212                let right = parts[1].trim().trim_matches('\'').trim_matches('"');
1213
1214                if left.starts_with("services.") {
1215                    let service_type = left.strip_prefix("services.").unwrap();
1216                    return Ok(context.get(service_type).map_or(false, |v| v == right));
1217                }
1218            }
1219        }
1220
1221        if condition.contains(" in ") {
1223            let parts: Vec<&str> = condition.split(" in ").collect();
1224            if parts.len() == 2 {
1225                let left = parts[0].trim();
1226                let right = parts[1].trim();
1227
1228                if left.starts_with("services.") && right.starts_with('[') && right.ends_with(']') {
1229                    let service_type = left.strip_prefix("services.").unwrap();
1230                    let options: Vec<&str> = right[1..right.len() - 1]
1231                        .split(',')
1232                        .map(|s| s.trim().trim_matches('\'').trim_matches('"'))
1233                        .collect();
1234
1235                    return Ok(context
1236                        .get(service_type)
1237                        .map_or(false, |v| options.contains(&v.as_str())));
1238                }
1239            }
1240        }
1241
1242        if condition.starts_with("has_") {
1244            return Ok(context.get(condition).map_or(false, |v| v == "true"));
1245        }
1246
1247        Ok(true)
1249    }
1250
1251    async fn evaluate_implicit_conditions(
1256        &self,
1257        file: &ComposedFile,
1258        context: &HashMap<String, String>,
1259    ) -> EngineResult<bool> {
1260        match &file.source {
1261            FileSource::BaseTemplate => {
1262                Ok(true)
1264            }
1265            FileSource::Service { category, provider } => {
1266                let category_key = format!("{:?}", category).to_lowercase();
1268                Ok(context
1269                    .get(&category_key)
1270                    .map_or(false, |selected| selected == provider))
1271            }
1272            FileSource::Merged => {
1273                Ok(true)
1275            }
1276        }
1277    }
1278
1279    async fn collect_environment_variables(
1283        &self,
1284        _services: &[ServiceSelection],
1285    ) -> EngineResult<HashMap<String, String>> {
1286        Ok(HashMap::new())
1288    }
1289}
1290
1291#[cfg(test)]
1292mod tests {
1293    use super::*;
1294    use tempfile::TempDir;
1295    use tokio::fs;
1296
1297    async fn create_test_structure() -> TempDir {
1298        let temp_dir = TempDir::new().unwrap();
1299        let base_path = temp_dir.path();
1300
1301        fs::create_dir_all(base_path.join("templates/test-app"))
1303            .await
1304            .unwrap();
1305        fs::write(
1306            base_path.join("templates/test-app/anvil.yaml"),
1307            r#"
1308name: "test-app"
1309description: "Test application"
1310version: "1.0.0"
1311services:
1312  - name: "auth"
1313    category: "auth"
1314    prompt: "Choose auth provider"
1315    options: ["clerk", "auth0"]
1316    required: true
1317"#,
1318        )
1319        .await
1320        .unwrap();
1321
1322        fs::create_dir_all(base_path.join("templates/shared/auth/clerk"))
1324            .await
1325            .unwrap();
1326        fs::write(
1327            base_path.join("templates/shared/auth/clerk/middleware.ts"),
1328            "// Clerk middleware",
1329        )
1330        .await
1331        .unwrap();
1332
1333        temp_dir
1334    }
1335
1336    #[tokio::test]
1337    async fn test_compose_template() {
1338        let temp_dir = create_test_structure().await;
1339        let base_path = temp_dir.path();
1340
1341        let engine = CompositionEngine::new(
1342            base_path.join("templates"),
1343            base_path.join("templates/shared"),
1344        );
1345
1346        let services = vec![ServiceSelection {
1347            category: ServiceCategory::Auth,
1348            provider: "clerk".to_string(),
1349            config: HashMap::new(),
1350        }];
1351
1352        let result = engine.compose_template("test-app", services).await;
1353        assert!(result.is_ok());
1354
1355        let composed = result.unwrap();
1356        assert_eq!(composed.base_config.name, "test-app");
1357        assert!(!composed.files.is_empty());
1358    }
1359}