Skip to main content

allframe_forge/templates/
acl.rs

1//! Anti-Corruption Layer archetype templates
2//!
3//! Templates for generating services that translate between legacy and modern
4//! systems.
5
6use crate::config::ProjectConfig;
7
8/// Convert a string to PascalCase
9fn to_pascal_case(s: &str) -> String {
10    s.split(|c| c == '-' || c == '_')
11        .map(|word| {
12            let mut chars = word.chars();
13            match chars.next() {
14                None => String::new(),
15                Some(first) => first.to_uppercase().chain(chars).collect(),
16            }
17        })
18        .collect()
19}
20
21/// Generate Cargo.toml for ACL project
22pub fn cargo_toml(config: &ProjectConfig) -> String {
23    let acl = config.acl.as_ref().unwrap();
24    let name = &config.name;
25
26    format!(
27        r#"[package]
28name = "{name}"
29version = "0.1.0"
30edition = "2021"
31rust-version = "1.89"
32description = "{display_name}"
33
34[dependencies]
35# AllFrame
36allframe-core = {{ version = "0.1", features = ["resilience", "otel"] }}
37
38# Web Framework
39axum = "0.7"
40tower = "0.5"
41tower-http = {{ version = "0.6", features = ["trace", "cors"] }}
42
43# HTTP Client
44reqwest = {{ version = "0.12", features = ["json", "rustls-tls"] }}
45
46# Async
47tokio = {{ version = "1", features = ["full"] }}
48async-trait = "0.1"
49futures = "0.3"
50
51# Serialization
52serde = {{ version = "1.0", features = ["derive"] }}
53serde_json = "1.0"
54
55# Error handling
56thiserror = "2.0"
57anyhow = "1.0"
58
59# Tracing & Metrics
60tracing = "0.1"
61tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
62opentelemetry = {{ version = "0.27", features = ["metrics"] }}
63
64# Utilities
65chrono = {{ version = "0.4", features = ["serde"] }}
66uuid = {{ version = "1.0", features = ["v4", "serde"] }}
67dotenvy = "0.15"
68
69[dev-dependencies]
70tokio-test = "0.4"
71mockall = "0.13"
72
73[[bin]]
74name = "{name}"
75path = "src/main.rs"
76"#,
77        name = name,
78        display_name = acl.display_name,
79    )
80}
81
82/// Generate main.rs
83pub fn main_rs(config: &ProjectConfig) -> String {
84    let acl = config.acl.as_ref().unwrap();
85    let pascal_name = to_pascal_case(&acl.service_name);
86
87    format!(
88        r#"//! {display_name}
89//!
90//! An anti-corruption layer service for translating between legacy and modern systems.
91
92use std::sync::Arc;
93use tracing::info;
94
95mod config;
96mod error;
97mod domain;
98mod application;
99mod infrastructure;
100mod presentation;
101
102use config::Config;
103use application::{pascal_name}Translator;
104use infrastructure::{{LegacyClient, HealthServer}};
105
106#[tokio::main]
107async fn main() -> anyhow::Result<()> {{
108    // Load environment variables
109    dotenvy::dotenv().ok();
110
111    // Initialize tracing
112    tracing_subscriber::fmt()
113        .with_env_filter(
114            tracing_subscriber::EnvFilter::from_default_env()
115                .add_directive(tracing::Level::INFO.into()),
116        )
117        .init();
118
119    // Load configuration
120    let config = Config::from_env();
121    info!("Starting {display_name}");
122    info!("Legacy system: {{}} ({{:?}})", config.legacy.name, config.legacy.connection_type);
123
124    // Create legacy client
125    let legacy_client = Arc::new(LegacyClient::new(&config.legacy));
126
127    // Create translator
128    let translator = Arc::new({pascal_name}Translator::new(config.clone(), legacy_client));
129
130    // Start health server in background
131    let health_port = config.server.health_port;
132    let health_handle = tokio::spawn(async move {{
133        let health_server = HealthServer::new(health_port);
134        health_server.run().await
135    }});
136
137    // Create router and start API server
138    let app = presentation::create_router(translator);
139
140    info!("Starting ACL server on port {{}}", config.server.http_port);
141    let listener = tokio::net::TcpListener::bind(
142        format!("0.0.0.0:{{}}", config.server.http_port)
143    ).await?;
144    axum::serve(listener, app).await?;
145
146    health_handle.abort();
147    info!("ACL shutdown complete");
148    Ok(())
149}}
150"#,
151        pascal_name = pascal_name,
152        display_name = acl.display_name,
153    )
154}
155
156/// Generate config.rs
157pub fn config_rs(config: &ProjectConfig) -> String {
158    let acl = config.acl.as_ref().unwrap();
159
160    format!(
161        r#"//! Service configuration
162
163use std::env;
164
165/// Main configuration
166#[derive(Debug, Clone)]
167pub struct Config {{
168    pub server: ServerConfig,
169    pub legacy: LegacyConfig,
170}}
171
172/// Server configuration
173#[derive(Debug, Clone)]
174pub struct ServerConfig {{
175    pub http_port: u16,
176    pub health_port: u16,
177}}
178
179/// Legacy system configuration
180#[derive(Debug, Clone)]
181pub struct LegacyConfig {{
182    pub name: String,
183    pub connection_type: ConnectionType,
184    pub connection_string: String,
185    pub timeout_ms: u64,
186}}
187
188/// Connection type for legacy system
189#[derive(Debug, Clone, Copy)]
190pub enum ConnectionType {{
191    Rest,
192    Soap,
193    Database,
194    File,
195    Mq,
196}}
197
198impl Config {{
199    pub fn from_env() -> Self {{
200        Self {{
201            server: ServerConfig {{
202                http_port: env::var("PORT")
203                    .unwrap_or_else(|_| "{http_port}".to_string())
204                    .parse()
205                    .expect("PORT must be a number"),
206                health_port: env::var("HEALTH_PORT")
207                    .unwrap_or_else(|_| "{health_port}".to_string())
208                    .parse()
209                    .expect("HEALTH_PORT must be a number"),
210            }},
211            legacy: LegacyConfig {{
212                name: env::var("LEGACY_NAME")
213                    .unwrap_or_else(|_| "{legacy_name}".to_string()),
214                connection_type: ConnectionType::Rest,
215                connection_string: env::var("LEGACY_URL")
216                    .unwrap_or_else(|_| "{legacy_url}".to_string()),
217                timeout_ms: env::var("LEGACY_TIMEOUT_MS")
218                    .unwrap_or_else(|_| "{timeout_ms}".to_string())
219                    .parse()
220                    .expect("LEGACY_TIMEOUT_MS must be a number"),
221            }},
222        }}
223    }}
224}}
225"#,
226        http_port = acl.server.http_port,
227        health_port = acl.server.health_port,
228        legacy_name = acl.legacy_system.name,
229        legacy_url = acl.legacy_system.connection_string,
230        timeout_ms = acl.legacy_system.timeout_ms,
231    )
232}
233
234/// Generate error.rs
235pub fn error_rs(config: &ProjectConfig) -> String {
236    let acl = config.acl.as_ref().unwrap();
237    let pascal_name = to_pascal_case(&acl.service_name);
238
239    format!(
240        r#"//! Error types
241
242use thiserror::Error;
243
244/// ACL errors
245#[derive(Error, Debug)]
246pub enum {pascal_name}Error {{
247    #[error("Legacy system error: {{0}}")]
248    LegacySystem(String),
249
250    #[error("Transformation error: {{0}}")]
251    Transformation(String),
252
253    #[error("Connection error: {{0}}")]
254    Connection(String),
255
256    #[error("Timeout error: {{0}}")]
257    Timeout(String),
258
259    #[error("Entity not found: {{0}}")]
260    NotFound(String),
261
262    #[error("Validation error: {{0}}")]
263    Validation(String),
264
265    #[error("Internal error: {{0}}")]
266    Internal(String),
267}}
268"#,
269        pascal_name = pascal_name,
270    )
271}
272
273/// Generate domain/mod.rs
274pub fn domain_mod(_config: &ProjectConfig) -> String {
275    r#"//! Domain layer
276
277pub mod legacy;
278pub mod modern;
279pub mod transformer;
280
281pub use legacy::*;
282pub use modern::*;
283pub use transformer::*;
284"#
285    .to_string()
286}
287
288/// Generate domain/legacy.rs
289pub fn domain_legacy(config: &ProjectConfig) -> String {
290    let acl = config.acl.as_ref().unwrap();
291    let source = acl
292        .transformations
293        .first()
294        .map(|t| &t.source)
295        .cloned()
296        .unwrap_or_else(|| "LegacyEntity".to_string());
297
298    format!(
299        r#"//! Legacy system domain models
300//!
301//! These models represent the data structures from the legacy system.
302//! They should match the legacy system's schema exactly.
303
304use chrono::{{DateTime, Utc}};
305use serde::{{Deserialize, Serialize}};
306
307/// Legacy entity from the old system
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct {source} {{
310    /// Legacy ID (often not a UUID)
311    pub id: String,
312    /// Name field (legacy format)
313    pub name: String,
314    /// Status code (legacy uses numeric codes)
315    pub status_code: i32,
316    /// Creation date (legacy format)
317    pub created_date: String,
318    /// Additional data (legacy uses generic map)
319    #[serde(default)]
320    pub extra_data: serde_json::Value,
321}}
322
323/// Legacy response wrapper
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct LegacyResponse<T> {{
326    pub success: bool,
327    pub data: Option<T>,
328    pub error_code: Option<i32>,
329    pub error_message: Option<String>,
330}}
331
332/// Legacy list response
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct LegacyListResponse<T> {{
335    pub items: Vec<T>,
336    pub total_count: i64,
337    pub page: i32,
338    pub page_size: i32,
339}}
340"#,
341        source = source,
342    )
343}
344
345/// Generate domain/modern.rs
346pub fn domain_modern(config: &ProjectConfig) -> String {
347    let acl = config.acl.as_ref().unwrap();
348    let target = acl
349        .transformations
350        .first()
351        .map(|t| &t.target)
352        .cloned()
353        .unwrap_or_else(|| "ModernEntity".to_string());
354
355    format!(
356        r#"//! Modern domain models
357//!
358//! These models represent the canonical domain models for the new system.
359//! They follow modern best practices and use proper types.
360
361use chrono::{{DateTime, Utc}};
362use serde::{{Deserialize, Serialize}};
363use uuid::Uuid;
364
365/// Modern entity in canonical format
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct {target} {{
368    /// UUID identifier
369    pub id: Uuid,
370    /// Name
371    pub name: String,
372    /// Status (enum-like)
373    pub status: EntityStatus,
374    /// Creation timestamp
375    pub created_at: DateTime<Utc>,
376    /// Last update timestamp
377    pub updated_at: Option<DateTime<Utc>>,
378    /// Metadata
379    #[serde(default)]
380    pub metadata: std::collections::HashMap<String, serde_json::Value>,
381}}
382
383/// Entity status
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386pub enum EntityStatus {{
387    Active,
388    Inactive,
389    Pending,
390    Archived,
391    Unknown,
392}}
393
394impl Default for EntityStatus {{
395    fn default() -> Self {{
396        Self::Unknown
397    }}
398}}
399
400/// Modern API response
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct ApiResponse<T> {{
403    pub data: T,
404    pub meta: ResponseMeta,
405}}
406
407/// Response metadata
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ResponseMeta {{
410    pub request_id: Uuid,
411    pub timestamp: DateTime<Utc>,
412}}
413
414/// Paginated response
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct PaginatedResponse<T> {{
417    pub data: Vec<T>,
418    pub pagination: Pagination,
419}}
420
421/// Pagination info
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct Pagination {{
424    pub total: i64,
425    pub page: i32,
426    pub per_page: i32,
427    pub total_pages: i32,
428}}
429"#,
430        target = target,
431    )
432}
433
434/// Generate domain/transformer.rs
435pub fn domain_transformer(config: &ProjectConfig) -> String {
436    let acl = config.acl.as_ref().unwrap();
437    let pascal_name = to_pascal_case(&acl.service_name);
438    // Source and target entity names (available for future customization)
439    let _source = acl
440        .transformations
441        .first()
442        .map(|t| &t.source)
443        .cloned()
444        .unwrap_or_else(|| "LegacyEntity".to_string());
445    let _target = acl
446        .transformations
447        .first()
448        .map(|t| &t.target)
449        .cloned()
450        .unwrap_or_else(|| "ModernEntity".to_string());
451
452    format!(
453        r#"//! Transformation traits
454
455use async_trait::async_trait;
456use crate::error::{pascal_name}Error;
457
458/// Trait for transforming legacy entities to modern format
459#[async_trait]
460pub trait LegacyToModern<L, M> {{
461    /// Transform a legacy entity to modern format
462    fn transform(&self, legacy: L) -> Result<M, {pascal_name}Error>;
463}}
464
465/// Trait for transforming modern entities to legacy format
466#[async_trait]
467pub trait ModernToLegacy<M, L> {{
468    /// Transform a modern entity to legacy format
469    fn transform(&self, modern: M) -> Result<L, {pascal_name}Error>;
470}}
471"#,
472        pascal_name = pascal_name,
473    )
474}
475
476/// Generate application/mod.rs
477pub fn application_mod(_config: &ProjectConfig) -> String {
478    r#"//! Application layer
479
480pub mod translator;
481
482pub use translator::*;
483"#
484    .to_string()
485}
486
487/// Generate application/translator.rs
488pub fn application_translator(config: &ProjectConfig) -> String {
489    let acl = config.acl.as_ref().unwrap();
490    let pascal_name = to_pascal_case(&acl.service_name);
491    let source = acl
492        .transformations
493        .first()
494        .map(|t| &t.source)
495        .cloned()
496        .unwrap_or_else(|| "LegacyEntity".to_string());
497    let target = acl
498        .transformations
499        .first()
500        .map(|t| &t.target)
501        .cloned()
502        .unwrap_or_else(|| "ModernEntity".to_string());
503
504    format!(
505        r#"//! Entity translator
506
507use std::sync::Arc;
508use chrono::{{DateTime, Utc, NaiveDateTime}};
509use tracing::{{info, warn}};
510use uuid::Uuid;
511
512use crate::config::Config;
513use crate::domain::{{
514    {source}, {target}, EntityStatus,
515    LegacyToModern, ModernToLegacy,
516}};
517use crate::error::{pascal_name}Error;
518use crate::infrastructure::LegacyClient;
519
520/// Main translator service
521pub struct {pascal_name}Translator {{
522    config: Config,
523    legacy_client: Arc<LegacyClient>,
524}}
525
526impl {pascal_name}Translator {{
527    pub fn new(config: Config, legacy_client: Arc<LegacyClient>) -> Self {{
528        Self {{
529            config,
530            legacy_client,
531        }}
532    }}
533
534    /// Get an entity from the legacy system and return in modern format
535    pub async fn get_entity(&self, id: &str) -> Result<{target}, {pascal_name}Error> {{
536        let legacy_entity = self.legacy_client.get_entity(id).await?;
537        self.transform(legacy_entity)
538    }}
539
540    /// List entities from the legacy system in modern format
541    pub async fn list_entities(&self, page: i32, per_page: i32) -> Result<Vec<{target}>, {pascal_name}Error> {{
542        let legacy_entities = self.legacy_client.list_entities(page, per_page).await?;
543        legacy_entities
544            .into_iter()
545            .map(|e| self.transform(e))
546            .collect()
547    }}
548
549    /// Create an entity in the legacy system from modern format
550    pub async fn create_entity(&self, modern: {target}) -> Result<{target}, {pascal_name}Error> {{
551        let legacy = self.to_legacy(modern)?;
552        let created = self.legacy_client.create_entity(legacy).await?;
553        self.transform(created)
554    }}
555
556    fn status_from_code(code: i32) -> EntityStatus {{
557        match code {{
558            1 => EntityStatus::Active,
559            2 => EntityStatus::Inactive,
560            3 => EntityStatus::Pending,
561            4 => EntityStatus::Archived,
562            _ => EntityStatus::Unknown,
563        }}
564    }}
565
566    fn status_to_code(status: EntityStatus) -> i32 {{
567        match status {{
568            EntityStatus::Active => 1,
569            EntityStatus::Inactive => 2,
570            EntityStatus::Pending => 3,
571            EntityStatus::Archived => 4,
572            EntityStatus::Unknown => 0,
573        }}
574    }}
575
576    fn to_legacy(&self, modern: {target}) -> Result<{source}, {pascal_name}Error> {{
577        Ok({source} {{
578            id: modern.id.to_string(),
579            name: modern.name,
580            status_code: Self::status_to_code(modern.status),
581            created_date: modern.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
582            extra_data: serde_json::to_value(modern.metadata)
583                .unwrap_or(serde_json::Value::Null),
584        }})
585    }}
586}}
587
588impl LegacyToModern<{source}, {target}> for {pascal_name}Translator {{
589    fn transform(&self, legacy: {source}) -> Result<{target}, {pascal_name}Error> {{
590        // Parse legacy ID to UUID (or generate new one if invalid)
591        let id = Uuid::parse_str(&legacy.id).unwrap_or_else(|_| {{
592            warn!(legacy_id = %legacy.id, "Invalid legacy ID, generating new UUID");
593            Uuid::new_v4()
594        }});
595
596        // Parse legacy date format
597        let created_at = NaiveDateTime::parse_from_str(&legacy.created_date, "%Y-%m-%d %H:%M:%S")
598            .map(|dt| dt.and_utc())
599            .unwrap_or_else(|_| {{
600                warn!(date = %legacy.created_date, "Invalid legacy date format");
601                Utc::now()
602            }});
603
604        // Convert status code to enum
605        let status = Self::status_from_code(legacy.status_code);
606
607        // Convert extra_data to metadata
608        let metadata = if let serde_json::Value::Object(map) = legacy.extra_data {{
609            map.into_iter().collect()
610        }} else {{
611            std::collections::HashMap::new()
612        }};
613
614        Ok({target} {{
615            id,
616            name: legacy.name,
617            status,
618            created_at,
619            updated_at: None,
620            metadata,
621        }})
622    }}
623}}
624"#,
625        pascal_name = pascal_name,
626        source = source,
627        target = target,
628    )
629}
630
631/// Generate infrastructure/mod.rs
632pub fn infrastructure_mod(_config: &ProjectConfig) -> String {
633    r#"//! Infrastructure layer
634
635pub mod legacy_client;
636pub mod health;
637
638pub use legacy_client::*;
639pub use health::*;
640"#
641    .to_string()
642}
643
644/// Generate infrastructure/legacy_client.rs
645pub fn infrastructure_legacy_client(config: &ProjectConfig) -> String {
646    let acl = config.acl.as_ref().unwrap();
647    let pascal_name = to_pascal_case(&acl.service_name);
648    let source = acl
649        .transformations
650        .first()
651        .map(|t| &t.source)
652        .cloned()
653        .unwrap_or_else(|| "LegacyEntity".to_string());
654
655    format!(
656        r#"//! Legacy system client
657
658use std::time::Duration;
659use reqwest::Client;
660use tracing::{{info, error}};
661
662use crate::config::LegacyConfig;
663use crate::domain::{source};
664use crate::error::{pascal_name}Error;
665
666/// Client for communicating with the legacy system
667pub struct LegacyClient {{
668    client: Client,
669    base_url: String,
670}}
671
672impl LegacyClient {{
673    pub fn new(config: &LegacyConfig) -> Self {{
674        let client = Client::builder()
675            .timeout(Duration::from_millis(config.timeout_ms))
676            .build()
677            .expect("Failed to create HTTP client");
678
679        Self {{
680            client,
681            base_url: config.connection_string.clone(),
682        }}
683    }}
684
685    pub async fn get_entity(&self, id: &str) -> Result<{source}, {pascal_name}Error> {{
686        let url = format!("{{}}/entities/{{}}", self.base_url, id);
687        info!(url = %url, "Fetching entity from legacy system");
688
689        let response = self
690            .client
691            .get(&url)
692            .send()
693            .await
694            .map_err(|e| {pascal_name}Error::Connection(e.to_string()))?;
695
696        if !response.status().is_success() {{
697            let status = response.status();
698            let body = response.text().await.unwrap_or_default();
699            error!(status = %status, body = %body, "Legacy system error");
700            return Err({pascal_name}Error::LegacySystem(format!(
701                "HTTP {{}}: {{}}",
702                status, body
703            )));
704        }}
705
706        response
707            .json::<{source}>()
708            .await
709            .map_err(|e| {pascal_name}Error::Transformation(e.to_string()))
710    }}
711
712    pub async fn list_entities(&self, page: i32, per_page: i32) -> Result<Vec<{source}>, {pascal_name}Error> {{
713        let url = format!(
714            "{{}}/entities?page={{}}&page_size={{}}",
715            self.base_url, page, per_page
716        );
717        info!(url = %url, "Listing entities from legacy system");
718
719        let response = self
720            .client
721            .get(&url)
722            .send()
723            .await
724            .map_err(|e| {pascal_name}Error::Connection(e.to_string()))?;
725
726        if !response.status().is_success() {{
727            let status = response.status();
728            let body = response.text().await.unwrap_or_default();
729            return Err({pascal_name}Error::LegacySystem(format!(
730                "HTTP {{}}: {{}}",
731                status, body
732            )));
733        }}
734
735        response
736            .json::<Vec<{source}>>()
737            .await
738            .map_err(|e| {pascal_name}Error::Transformation(e.to_string()))
739    }}
740
741    pub async fn create_entity(&self, entity: {source}) -> Result<{source}, {pascal_name}Error> {{
742        let url = format!("{{}}/entities", self.base_url);
743        info!(url = %url, "Creating entity in legacy system");
744
745        let response = self
746            .client
747            .post(&url)
748            .json(&entity)
749            .send()
750            .await
751            .map_err(|e| {pascal_name}Error::Connection(e.to_string()))?;
752
753        if !response.status().is_success() {{
754            let status = response.status();
755            let body = response.text().await.unwrap_or_default();
756            return Err({pascal_name}Error::LegacySystem(format!(
757                "HTTP {{}}: {{}}",
758                status, body
759            )));
760        }}
761
762        response
763            .json::<{source}>()
764            .await
765            .map_err(|e| {pascal_name}Error::Transformation(e.to_string()))
766    }}
767}}
768"#,
769        pascal_name = pascal_name,
770        source = source,
771    )
772}
773
774/// Generate infrastructure/health.rs
775pub fn infrastructure_health(_config: &ProjectConfig) -> String {
776    r#"//! Health check server
777
778use axum::{routing::get, Router};
779use tracing::info;
780
781/// Health check server for Kubernetes probes
782pub struct HealthServer {
783    port: u16,
784}
785
786impl HealthServer {
787    pub fn new(port: u16) -> Self {
788        Self { port }
789    }
790
791    pub async fn run(&self) -> Result<(), std::io::Error> {
792        let app = Router::new()
793            .route("/health", get(health))
794            .route("/ready", get(ready));
795
796        let addr = format!("0.0.0.0:{}", self.port);
797        info!("Health server listening on {}", addr);
798
799        let listener = tokio::net::TcpListener::bind(&addr).await?;
800        axum::serve(listener, app).await
801    }
802}
803
804async fn health() -> &'static str {
805    "OK"
806}
807
808async fn ready() -> &'static str {
809    "OK"
810}
811"#
812    .to_string()
813}
814
815/// Generate presentation/mod.rs
816pub fn presentation_mod(_config: &ProjectConfig) -> String {
817    r#"//! Presentation layer
818
819pub mod handlers;
820
821pub use handlers::*;
822"#
823    .to_string()
824}
825
826/// Generate presentation/handlers.rs
827pub fn presentation_handlers(config: &ProjectConfig) -> String {
828    let acl = config.acl.as_ref().unwrap();
829    let pascal_name = to_pascal_case(&acl.service_name);
830    let target = acl
831        .transformations
832        .first()
833        .map(|t| &t.target)
834        .cloned()
835        .unwrap_or_else(|| "ModernEntity".to_string());
836
837    format!(
838        r#"//! API handlers
839
840use std::sync::Arc;
841use axum::{{
842    extract::{{Path, Query, State}},
843    http::StatusCode,
844    response::IntoResponse,
845    routing::{{get, post}},
846    Json, Router,
847}};
848use serde::Deserialize;
849use chrono::Utc;
850use uuid::Uuid;
851
852use crate::application::{pascal_name}Translator;
853use crate::domain::{{ApiResponse, ResponseMeta, PaginatedResponse, Pagination, {target}}};
854
855type AppState = Arc<{pascal_name}Translator>;
856
857/// Create the API router
858pub fn create_router(translator: Arc<{pascal_name}Translator>) -> Router {{
859    Router::new()
860        .route("/api/v1/entities", get(list_entities).post(create_entity))
861        .route("/api/v1/entities/:id", get(get_entity))
862        .with_state(translator)
863}}
864
865#[derive(Debug, Deserialize)]
866struct ListParams {{
867    #[serde(default = "default_page")]
868    page: i32,
869    #[serde(default = "default_per_page")]
870    per_page: i32,
871}}
872
873fn default_page() -> i32 {{ 1 }}
874fn default_per_page() -> i32 {{ 20 }}
875
876async fn get_entity(
877    State(translator): State<AppState>,
878    Path(id): Path<String>,
879) -> impl IntoResponse {{
880    match translator.get_entity(&id).await {{
881        Ok(entity) => (
882            StatusCode::OK,
883            Json(ApiResponse {{
884                data: entity,
885                meta: ResponseMeta {{
886                    request_id: Uuid::new_v4(),
887                    timestamp: Utc::now(),
888                }},
889            }}),
890        )
891            .into_response(),
892        Err(e) => (
893            StatusCode::INTERNAL_SERVER_ERROR,
894            Json(serde_json::json!({{ "error": e.to_string() }})),
895        )
896            .into_response(),
897    }}
898}}
899
900async fn list_entities(
901    State(translator): State<AppState>,
902    Query(params): Query<ListParams>,
903) -> impl IntoResponse {{
904    match translator.list_entities(params.page, params.per_page).await {{
905        Ok(entities) => {{
906            let total = entities.len() as i64;
907            let total_pages = ((total as f64) / (params.per_page as f64)).ceil() as i32;
908            (
909                StatusCode::OK,
910                Json(PaginatedResponse {{
911                    data: entities,
912                    pagination: Pagination {{
913                        total,
914                        page: params.page,
915                        per_page: params.per_page,
916                        total_pages,
917                    }},
918                }}),
919            )
920                .into_response()
921        }}
922        Err(e) => (
923            StatusCode::INTERNAL_SERVER_ERROR,
924            Json(serde_json::json!({{ "error": e.to_string() }})),
925        )
926            .into_response(),
927    }}
928}}
929
930async fn create_entity(
931    State(translator): State<AppState>,
932    Json(entity): Json<{target}>,
933) -> impl IntoResponse {{
934    match translator.create_entity(entity).await {{
935        Ok(created) => (
936            StatusCode::CREATED,
937            Json(ApiResponse {{
938                data: created,
939                meta: ResponseMeta {{
940                    request_id: Uuid::new_v4(),
941                    timestamp: Utc::now(),
942                }},
943            }}),
944        )
945            .into_response(),
946        Err(e) => (
947            StatusCode::INTERNAL_SERVER_ERROR,
948            Json(serde_json::json!({{ "error": e.to_string() }})),
949        )
950            .into_response(),
951    }}
952}}
953"#,
954        pascal_name = pascal_name,
955        target = target,
956    )
957}
958
959/// Generate README.md
960pub fn readme(config: &ProjectConfig) -> String {
961    let acl = config.acl.as_ref().unwrap();
962    let name = &config.name;
963
964    let transform_table: Vec<String> = acl
965        .transformations
966        .iter()
967        .map(|t| format!("| {} | {} | {} |", t.source, t.target, t.description))
968        .collect();
969
970    format!(
971        r#"# {display_name}
972
973An anti-corruption layer service built with AllFrame for translating between legacy and modern systems.
974
975## Features
976
977- **Bidirectional Translation**: Convert between legacy and modern formats
978- **Clean Separation**: Isolate legacy system complexity from modern services
979- **Type Safety**: Strong typing for both legacy and modern models
980- **OpenTelemetry**: Distributed tracing and metrics
981- **Health Checks**: Kubernetes-ready liveness and readiness probes
982
983## Prerequisites
984
985- Rust 1.75+
986
987## Configuration
988
989Set the following environment variables:
990
991```bash
992# Server
993PORT=8080
994HEALTH_PORT=8081
995
996# Legacy System
997LEGACY_NAME=legacy_api
998LEGACY_URL=http://legacy-system:8080
999LEGACY_TIMEOUT_MS=10000
1000```
1001
1002## Transformations
1003
1004| Source (Legacy) | Target (Modern) | Description |
1005|-----------------|-----------------|-------------|
1006{transform_table}
1007
1008## Running
1009
1010```bash
1011# Development
1012cargo run
1013
1014# Production
1015cargo build --release
1016./target/release/{name}
1017```
1018
1019## API Endpoints
1020
1021| Method | Endpoint | Description |
1022|--------|----------|-------------|
1023| GET | /api/v1/entities | List entities (paginated) |
1024| GET | /api/v1/entities/:id | Get entity by ID |
1025| POST | /api/v1/entities | Create entity |
1026
1027## Architecture
1028
1029```
1030┌─────────────────────────────────────────────────────────────┐
1031│                  Anti-Corruption Layer                       │
1032│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐  │
1033│  │  Modern API │───▶│  Translator │───▶│  Legacy Client  │  │
1034│  └─────────────┘    └─────────────┘    └─────────────────┘  │
1035│        │                  │                    │            │
1036│        ▼                  ▼                    ▼            │
1037│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────┐  │
1038│  │   Modern    │    │   Domain    │    │     Legacy      │  │
1039│  │   Models    │◀──▶│  Transform  │◀──▶│     Models      │  │
1040│  └─────────────┘    └─────────────┘    └─────────────────┘  │
1041└─────────────────────────────────────────────────────────────┘
104210431044                    ┌───────────────────┐
1045                    │   Legacy System   │
1046                    │   (External)      │
1047                    └───────────────────┘
1048```
1049
1050## Adding New Transformations
1051
10521. Define the legacy model in `src/domain/legacy.rs`
10532. Define the modern model in `src/domain/modern.rs`
10543. Implement the `LegacyToModern` trait in the translator
10554. Add API endpoints as needed
1056
1057## License
1058
1059MIT
1060"#,
1061        display_name = acl.display_name,
1062        name = name,
1063        transform_table = transform_table.join("\n"),
1064    )
1065}
1066
1067/// Generate Dockerfile
1068pub fn dockerfile(config: &ProjectConfig) -> String {
1069    let name = &config.name;
1070
1071    format!(
1072        r#"FROM rust:1.75-slim as builder
1073
1074WORKDIR /app
1075
1076# Install dependencies
1077RUN apt-get update && apt-get install -y \
1078    pkg-config \
1079    libssl-dev \
1080    && rm -rf /var/lib/apt/lists/*
1081
1082# Copy manifests
1083COPY Cargo.toml Cargo.lock ./
1084
1085# Create dummy main to cache dependencies
1086RUN mkdir src && echo "fn main() {{}}" > src/main.rs
1087RUN cargo build --release
1088RUN rm -rf src
1089
1090# Copy source
1091COPY src ./src
1092
1093# Build
1094RUN touch src/main.rs && cargo build --release
1095
1096# Runtime image
1097FROM debian:bookworm-slim
1098
1099RUN apt-get update && apt-get install -y \
1100    ca-certificates \
1101    libssl3 \
1102    && rm -rf /var/lib/apt/lists/*
1103
1104WORKDIR /app
1105
1106COPY --from=builder /app/target/release/{name} /app/
1107
1108ENV PORT=8080
1109ENV HEALTH_PORT=8081
1110EXPOSE 8080 8081
1111
1112CMD ["/app/{name}"]
1113"#,
1114        name = name,
1115    )
1116}
1117
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121
1122    #[test]
1123    fn test_to_pascal_case() {
1124        assert_eq!(to_pascal_case("acl"), "Acl");
1125        assert_eq!(
1126            to_pascal_case("anti_corruption_layer"),
1127            "AntiCorruptionLayer"
1128        );
1129        assert_eq!(to_pascal_case("simple"), "Simple");
1130    }
1131}