hemmer_provider_generator_generator/
lib.rs

1//! Code and manifest generation for Hemmer providers
2//!
3//! This crate transforms parsed SDK definitions into provider artifacts
4//! including JCL manifests, Rust code, and tests.
5//!
6//! Generated providers use the `hemmer-provider-sdk` for gRPC communication.
7
8mod templates;
9
10use hemmer_provider_generator_common::{
11    GeneratorError, ProviderDefinition, Result, ServiceDefinition,
12};
13use std::fs;
14use std::path::Path;
15use tera::Tera;
16
17/// Provider generator
18///
19/// Transforms ServiceDefinition IR into complete provider package:
20/// - provider.jcf (JCL manifest)
21/// - src/main.rs (binary entry point)
22/// - src/lib.rs (ProviderService implementation)
23/// - src/resources/*.rs (resource handlers)
24/// - Cargo.toml
25/// - README.md
26pub struct ProviderGenerator {
27    service_def: ServiceDefinition,
28    tera: Tera,
29}
30
31impl ProviderGenerator {
32    /// Create a new provider generator from ServiceDefinition
33    pub fn new(service_def: ServiceDefinition) -> Result<Self> {
34        let tera = templates::load_templates()?;
35        Ok(Self { service_def, tera })
36    }
37
38    /// Generate all provider artifacts to a directory
39    pub fn generate_to_directory(&self, output_dir: &Path) -> Result<()> {
40        // Create output directory structure
41        fs::create_dir_all(output_dir).map_err(|e| {
42            GeneratorError::Generation(format!("Failed to create output directory: {}", e))
43        })?;
44
45        let src_dir = output_dir.join("src");
46        fs::create_dir_all(&src_dir).map_err(|e| {
47            GeneratorError::Generation(format!("Failed to create src directory: {}", e))
48        })?;
49
50        let resources_dir = src_dir.join("resources");
51        fs::create_dir_all(&resources_dir).map_err(|e| {
52            GeneratorError::Generation(format!("Failed to create resources directory: {}", e))
53        })?;
54
55        // Generate all artifacts
56        self.generate_provider_jcf(output_dir)?;
57        self.generate_cargo_toml(output_dir)?;
58        self.generate_main_rs(&src_dir)?;
59        self.generate_lib_rs(&src_dir)?;
60        self.generate_resources(&resources_dir)?;
61        self.generate_readme(output_dir)?;
62
63        Ok(())
64    }
65
66    /// Generate provider.jcf (JCL manifest)
67    fn generate_provider_jcf(&self, output_dir: &Path) -> Result<()> {
68        let context = self.create_context();
69        let rendered = self
70            .tera
71            .render("provider.jcf", &context)
72            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
73
74        let output_path = output_dir.join("provider.jcf");
75        fs::write(output_path, rendered).map_err(|e| {
76            GeneratorError::Generation(format!("Failed to write provider.jcf: {}", e))
77        })?;
78
79        Ok(())
80    }
81
82    /// Generate main.rs (binary entry point)
83    fn generate_main_rs(&self, src_dir: &Path) -> Result<()> {
84        let context = self.create_context();
85        let rendered = self
86            .tera
87            .render("main.rs", &context)
88            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
89
90        let output_path = src_dir.join("main.rs");
91        fs::write(output_path, rendered)
92            .map_err(|e| GeneratorError::Generation(format!("Failed to write main.rs: {}", e)))?;
93
94        Ok(())
95    }
96
97    /// Generate Cargo.toml
98    fn generate_cargo_toml(&self, output_dir: &Path) -> Result<()> {
99        let context = self.create_context();
100        let rendered = self
101            .tera
102            .render("Cargo.toml", &context)
103            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
104
105        let output_path = output_dir.join("Cargo.toml");
106        fs::write(output_path, rendered).map_err(|e| {
107            GeneratorError::Generation(format!("Failed to write Cargo.toml: {}", e))
108        })?;
109
110        Ok(())
111    }
112
113    /// Generate lib.rs
114    fn generate_lib_rs(&self, src_dir: &Path) -> Result<()> {
115        let context = self.create_context();
116        let rendered = self
117            .tera
118            .render("lib.rs", &context)
119            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
120
121        let output_path = src_dir.join("lib.rs");
122        fs::write(output_path, rendered)
123            .map_err(|e| GeneratorError::Generation(format!("Failed to write lib.rs: {}", e)))?;
124
125        Ok(())
126    }
127
128    /// Generate resource modules
129    fn generate_resources(&self, resources_dir: &Path) -> Result<()> {
130        for resource in &self.service_def.resources {
131            let mut context = self.create_context();
132            context.insert("resource", resource);
133
134            let rendered = self
135                .tera
136                .render("resource.rs", &context)
137                .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
138
139            let output_path = resources_dir.join(format!("{}.rs", resource.name));
140            fs::write(output_path, rendered).map_err(|e| {
141                GeneratorError::Generation(format!(
142                    "Failed to write resource {}.rs: {}",
143                    resource.name, e
144                ))
145            })?;
146        }
147
148        // Generate mod.rs for resources
149        let mut context = self.create_context();
150        let resource_names: Vec<&str> = self
151            .service_def
152            .resources
153            .iter()
154            .map(|r| r.name.as_str())
155            .collect();
156        context.insert("resource_names", &resource_names);
157
158        let rendered = self
159            .tera
160            .render("resources_mod.rs", &context)
161            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
162
163        let output_path = resources_dir.join("mod.rs");
164        fs::write(output_path, rendered).map_err(|e| {
165            GeneratorError::Generation(format!("Failed to write resources/mod.rs: {}", e))
166        })?;
167
168        Ok(())
169    }
170
171    /// Generate README.md
172    fn generate_readme(&self, output_dir: &Path) -> Result<()> {
173        let context = self.create_context();
174        let rendered = self
175            .tera
176            .render("README.md", &context)
177            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
178
179        let output_path = output_dir.join("README.md");
180        fs::write(output_path, rendered)
181            .map_err(|e| GeneratorError::Generation(format!("Failed to write README.md: {}", e)))?;
182
183        Ok(())
184    }
185
186    /// Create template context from ServiceDefinition
187    fn create_context(&self) -> tera::Context {
188        let mut context = tera::Context::new();
189        context.insert("service", &self.service_def);
190        context.insert("provider", &format!("{:?}", self.service_def.provider));
191        context.insert("service_name", &self.service_def.name);
192        context.insert("sdk_version", &self.service_def.sdk_version);
193        context.insert("resources", &self.service_def.resources);
194        context.insert("data_sources", &self.service_def.data_sources);
195
196        // Add SDK configuration for provider-agnostic template generation
197        let sdk_config = self.service_def.provider.sdk_config();
198        context.insert("sdk_config", &sdk_config);
199        context.insert("config_attrs", &sdk_config.config_attrs);
200        context.insert(
201            "uses_shared_client",
202            &self.service_def.provider.uses_shared_client(),
203        );
204
205        context
206    }
207}
208
209/// Generate provider artifacts (convenience function)
210pub fn generate_provider(service_def: ServiceDefinition, output_path: &str) -> Result<()> {
211    let generator = ProviderGenerator::new(service_def)?;
212    generator.generate_to_directory(Path::new(output_path))
213}
214
215/// Unified provider generator for multi-service providers
216///
217/// Transforms ProviderDefinition IR into complete unified provider package:
218/// - provider.jcf (JCL manifest)
219/// - src/main.rs (binary entry point)
220/// - src/lib.rs (ProviderService implementation)
221/// - src/{service}/mod.rs (service handlers)
222/// - src/{service}/resources/*.rs (resource handlers)
223/// - Cargo.toml
224/// - README.md
225pub struct UnifiedProviderGenerator {
226    provider_def: ProviderDefinition,
227    tera: Tera,
228}
229
230impl UnifiedProviderGenerator {
231    /// Create a new unified provider generator from ProviderDefinition
232    pub fn new(provider_def: ProviderDefinition) -> Result<Self> {
233        let tera = templates::load_unified_templates()?;
234        Ok(Self { provider_def, tera })
235    }
236
237    /// Generate all provider artifacts to a directory
238    pub fn generate_to_directory(&self, output_dir: &Path) -> Result<()> {
239        // Create output directory structure
240        fs::create_dir_all(output_dir).map_err(|e| {
241            GeneratorError::Generation(format!("Failed to create output directory: {}", e))
242        })?;
243
244        let src_dir = output_dir.join("src");
245        fs::create_dir_all(&src_dir).map_err(|e| {
246            GeneratorError::Generation(format!("Failed to create src directory: {}", e))
247        })?;
248
249        // Generate top-level artifacts
250        self.generate_unified_provider_jcf(output_dir)?;
251        self.generate_unified_cargo_toml(output_dir)?;
252        self.generate_unified_main_rs(&src_dir)?;
253        self.generate_unified_lib_rs(&src_dir)?;
254        self.generate_unified_readme(output_dir)?;
255        self.generate_release_workflow(output_dir)?;
256        self.generate_docs(output_dir)?;
257
258        // Generate service modules
259        for service in &self.provider_def.services {
260            let service_dir = src_dir.join(&service.name);
261            fs::create_dir_all(&service_dir).map_err(|e| {
262                GeneratorError::Generation(format!(
263                    "Failed to create service directory {}: {}",
264                    service.name, e
265                ))
266            })?;
267
268            let resources_dir = service_dir.join("resources");
269            fs::create_dir_all(&resources_dir).map_err(|e| {
270                GeneratorError::Generation(format!(
271                    "Failed to create resources directory for {}: {}",
272                    service.name, e
273                ))
274            })?;
275
276            self.generate_service_mod(&service_dir, service)?;
277            self.generate_service_resources(&resources_dir, service)?;
278        }
279
280        Ok(())
281    }
282
283    /// Generate unified provider.jcf (JCL manifest)
284    fn generate_unified_provider_jcf(&self, output_dir: &Path) -> Result<()> {
285        let context = self.create_unified_context();
286        let rendered = self
287            .tera
288            .render("unified_provider.jcf", &context)
289            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
290
291        let output_path = output_dir.join("provider.jcf");
292        fs::write(output_path, rendered).map_err(|e| {
293            GeneratorError::Generation(format!("Failed to write provider.jcf: {}", e))
294        })?;
295
296        Ok(())
297    }
298
299    /// Generate unified main.rs (binary entry point)
300    fn generate_unified_main_rs(&self, src_dir: &Path) -> Result<()> {
301        let context = self.create_unified_context();
302        let rendered = self
303            .tera
304            .render("unified_main.rs", &context)
305            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
306
307        let output_path = src_dir.join("main.rs");
308        fs::write(output_path, rendered)
309            .map_err(|e| GeneratorError::Generation(format!("Failed to write main.rs: {}", e)))?;
310
311        Ok(())
312    }
313
314    /// Generate unified Cargo.toml
315    fn generate_unified_cargo_toml(&self, output_dir: &Path) -> Result<()> {
316        let context = self.create_unified_context();
317        let rendered = self
318            .tera
319            .render("unified_Cargo.toml", &context)
320            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
321
322        let output_path = output_dir.join("Cargo.toml");
323        fs::write(output_path, rendered).map_err(|e| {
324            GeneratorError::Generation(format!("Failed to write Cargo.toml: {}", e))
325        })?;
326
327        Ok(())
328    }
329
330    /// Generate unified lib.rs
331    fn generate_unified_lib_rs(&self, src_dir: &Path) -> Result<()> {
332        let context = self.create_unified_context();
333        let rendered = self
334            .tera
335            .render("unified_lib.rs", &context)
336            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
337
338        let output_path = src_dir.join("lib.rs");
339        fs::write(output_path, rendered)
340            .map_err(|e| GeneratorError::Generation(format!("Failed to write lib.rs: {}", e)))?;
341
342        Ok(())
343    }
344
345    /// Generate service module
346    fn generate_service_mod(&self, service_dir: &Path, service: &ServiceDefinition) -> Result<()> {
347        let mut context = self.create_unified_context();
348        context.insert("service", service);
349
350        let rendered = self
351            .tera
352            .render("unified_service.rs", &context)
353            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
354
355        let output_path = service_dir.join("mod.rs");
356        fs::write(output_path, rendered).map_err(|e| {
357            GeneratorError::Generation(format!("Failed to write {}/mod.rs: {}", service.name, e))
358        })?;
359
360        Ok(())
361    }
362
363    /// Generate service resources
364    fn generate_service_resources(
365        &self,
366        resources_dir: &Path,
367        service: &ServiceDefinition,
368    ) -> Result<()> {
369        // Generate individual resource files
370        for resource in &service.resources {
371            let mut context = self.create_unified_context();
372            context.insert("service", service);
373            context.insert("service_name", &service.name);
374            context.insert("resource", resource);
375
376            let rendered = self
377                .tera
378                .render("unified_resource.rs", &context)
379                .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
380
381            let output_path = resources_dir.join(format!("{}.rs", resource.name));
382            fs::write(output_path, rendered).map_err(|e| {
383                GeneratorError::Generation(format!(
384                    "Failed to write resource {}.rs: {}",
385                    resource.name, e
386                ))
387            })?;
388        }
389
390        // Generate resources/mod.rs
391        let mut context = self.create_unified_context();
392        let resource_names: Vec<&str> = service.resources.iter().map(|r| r.name.as_str()).collect();
393        context.insert("resource_names", &resource_names);
394        context.insert("is_unified", &true);
395
396        let rendered = self
397            .tera
398            .render("resources_mod.rs", &context)
399            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
400
401        let output_path = resources_dir.join("mod.rs");
402        fs::write(output_path, rendered).map_err(|e| {
403            GeneratorError::Generation(format!(
404                "Failed to write {}/resources/mod.rs: {}",
405                service.name, e
406            ))
407        })?;
408
409        Ok(())
410    }
411
412    /// Generate README.md
413    fn generate_unified_readme(&self, output_dir: &Path) -> Result<()> {
414        let context = self.create_unified_context();
415        let rendered = self
416            .tera
417            .render("unified_README.md", &context)
418            .map_err(|e| GeneratorError::Generation(format!("Template error: {}", e)))?;
419
420        let output_path = output_dir.join("README.md");
421        fs::write(output_path, rendered)
422            .map_err(|e| GeneratorError::Generation(format!("Failed to write README.md: {}", e)))?;
423
424        Ok(())
425    }
426
427    /// Generate GitHub Actions release workflow
428    fn generate_release_workflow(&self, output_dir: &Path) -> Result<()> {
429        // Create .github/workflows directory
430        let workflows_dir = output_dir.join(".github").join("workflows");
431        fs::create_dir_all(&workflows_dir).map_err(|e| {
432            GeneratorError::Generation(format!(
433                "Failed to create .github/workflows directory: {}",
434                e
435            ))
436        })?;
437
438        let context = self.create_unified_context();
439        let rendered = self
440            .tera
441            .render("release.yml", &context)
442            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
443
444        let output_path = workflows_dir.join("release.yml");
445        fs::write(output_path, rendered).map_err(|e| {
446            GeneratorError::Generation(format!("Failed to write release.yml: {}", e))
447        })?;
448
449        Ok(())
450    }
451
452    /// Generate documentation in docs/ directory
453    fn generate_docs(&self, output_dir: &Path) -> Result<()> {
454        // Create docs directory structure
455        let docs_dir = output_dir.join("docs");
456        fs::create_dir_all(&docs_dir).map_err(|e| {
457            GeneratorError::Generation(format!("Failed to create docs directory: {}", e))
458        })?;
459
460        let services_docs_dir = docs_dir.join("services");
461        fs::create_dir_all(&services_docs_dir).map_err(|e| {
462            GeneratorError::Generation(format!("Failed to create docs/services directory: {}", e))
463        })?;
464
465        // Generate installation.md
466        self.generate_installation_docs(&docs_dir)?;
467
468        // Generate getting-started.md
469        self.generate_getting_started_docs(&docs_dir)?;
470
471        // Generate service-specific docs
472        for service in &self.provider_def.services {
473            self.generate_service_docs(&services_docs_dir, service)?;
474        }
475
476        Ok(())
477    }
478
479    /// Generate docs/installation.md
480    fn generate_installation_docs(&self, docs_dir: &Path) -> Result<()> {
481        let context = self.create_unified_context();
482        let rendered = self
483            .tera
484            .render("docs_installation.md", &context)
485            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
486
487        let output_path = docs_dir.join("installation.md");
488        fs::write(output_path, rendered).map_err(|e| {
489            GeneratorError::Generation(format!("Failed to write installation.md: {}", e))
490        })?;
491
492        Ok(())
493    }
494
495    /// Generate docs/getting-started.md
496    fn generate_getting_started_docs(&self, docs_dir: &Path) -> Result<()> {
497        let context = self.create_unified_context();
498        let rendered = self
499            .tera
500            .render("docs_getting_started.md", &context)
501            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
502
503        let output_path = docs_dir.join("getting-started.md");
504        fs::write(output_path, rendered).map_err(|e| {
505            GeneratorError::Generation(format!("Failed to write getting-started.md: {}", e))
506        })?;
507
508        Ok(())
509    }
510
511    /// Generate docs/services/{service}.md
512    fn generate_service_docs(
513        &self,
514        services_docs_dir: &Path,
515        service: &ServiceDefinition,
516    ) -> Result<()> {
517        let mut context = self.create_unified_context();
518        context.insert("service", service);
519
520        let rendered = self
521            .tera
522            .render("docs_service.md", &context)
523            .map_err(|e| GeneratorError::Generation(format!("Template error: {:?}", e)))?;
524
525        let output_path = services_docs_dir.join(format!("{}.md", service.name));
526        fs::write(output_path, rendered).map_err(|e| {
527            GeneratorError::Generation(format!("Failed to write {}.md: {}", service.name, e))
528        })?;
529
530        Ok(())
531    }
532
533    /// Create template context from ProviderDefinition
534    fn create_unified_context(&self) -> tera::Context {
535        let mut context = tera::Context::new();
536        context.insert("provider", &format!("{:?}", self.provider_def.provider));
537        context.insert("provider_name", &self.provider_def.provider_name);
538        context.insert("sdk_version", &self.provider_def.sdk_version);
539        context.insert("services", &self.provider_def.services);
540
541        // Calculate total resources
542        let total_resources: usize = self
543            .provider_def
544            .services
545            .iter()
546            .map(|s| s.resources.len())
547            .sum();
548        context.insert("total_resources", &total_resources);
549
550        // Add SDK configuration for provider-agnostic template generation
551        let sdk_config = self.provider_def.provider.sdk_config();
552        context.insert("sdk_config", &sdk_config);
553        context.insert("config_attrs", &sdk_config.config_attrs);
554        context.insert(
555            "uses_shared_client",
556            &self.provider_def.provider.uses_shared_client(),
557        );
558
559        context
560    }
561}
562
563/// Generate unified provider artifacts (convenience function)
564pub fn generate_unified_provider(
565    provider_def: ProviderDefinition,
566    output_path: &str,
567) -> Result<()> {
568    let generator = UnifiedProviderGenerator::new(provider_def)?;
569    generator.generate_to_directory(Path::new(output_path))
570}
571
572#[cfg(test)]
573mod tests {
574    use super::*;
575    use hemmer_provider_generator_common::Provider;
576
577    #[test]
578    fn test_generator_creation() {
579        let service_def = ServiceDefinition {
580            provider: Provider::Aws,
581            name: "s3".to_string(),
582            sdk_version: "1.0.0".to_string(),
583            resources: vec![],
584            data_sources: vec![], // Will implement data source detection later
585        };
586
587        let result = ProviderGenerator::new(service_def);
588        assert!(result.is_ok());
589    }
590
591    #[test]
592    fn test_unified_generator_creation() {
593        let provider_def = ProviderDefinition {
594            provider: Provider::Aws,
595            provider_name: "aws".to_string(),
596            sdk_version: "1.0.0".to_string(),
597            services: vec![],
598        };
599
600        let result = UnifiedProviderGenerator::new(provider_def);
601        if let Err(e) = &result {
602            eprintln!("Error creating generator: {:?}", e);
603        }
604        assert!(result.is_ok());
605    }
606}