1use crate::config::ProjectConfig;
7
8fn 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
21pub 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
82pub 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
156pub 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
234pub 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
273pub 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
288pub 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
345pub 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
434pub 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 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
476pub fn application_mod(_config: &ProjectConfig) -> String {
478 r#"//! Application layer
479
480pub mod translator;
481
482pub use translator::*;
483"#
484 .to_string()
485}
486
487pub 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
631pub 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
644pub 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
774pub 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
815pub fn presentation_mod(_config: &ProjectConfig) -> String {
817 r#"//! Presentation layer
818
819pub mod handlers;
820
821pub use handlers::*;
822"#
823 .to_string()
824}
825
826pub 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
959pub 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└─────────────────────────────────────────────────────────────┘
1042 │
1043 ▼
1044 ┌───────────────────┐
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
1067pub 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}