allframe_forge/
lib.rs

1//! AllFrame Forge - Project scaffolding library
2//!
3//! This library provides utilities for creating new AllFrame projects.
4//! It is used by the `allframe` CLI binary.
5//!
6//! # Archetypes
7//!
8//! AllFrame supports different project archetypes:
9//! - `basic` (default): Simple Clean Architecture project with greeter example
10//! - `gateway`: API Gateway service with gRPC, resilience, and caching
11//! - `consumer`: Event consumer service with Kafka, idempotency, and DLQ
12//! - `producer`: Event producer service with outbox pattern and transactional messaging
13//! - `bff`: Backend for Frontend API aggregation service
14//! - `scheduled`: Scheduled jobs service with cron-based task execution
15//! - `websocket-gateway`: WebSocket gateway for real-time bidirectional communication
16//! - `saga-orchestrator`: Saga orchestrator for distributed transaction coordination
17//! - `legacy-adapter`: Legacy system adapter (anti-corruption layer)
18//!
19//! # Usage
20//!
21//! ```bash
22//! # Create a basic project
23//! allframe ignite my-service
24//!
25//! # Create a gateway project
26//! allframe ignite my-gateway --archetype gateway
27//!
28//! # Create a consumer project
29//! allframe ignite my-consumer --archetype consumer
30//!
31//! # Create a producer project
32//! allframe ignite my-producer --archetype producer
33//!
34//! # Create a BFF project
35//! allframe ignite my-bff --archetype bff
36//!
37//! # Create a scheduled jobs project
38//! allframe ignite my-scheduler --archetype scheduled
39//!
40//! # Create a WebSocket gateway project
41//! allframe ignite my-ws --archetype websocket-gateway
42//!
43//! # Create a saga orchestrator project
44//! allframe ignite my-saga --archetype saga-orchestrator
45//!
46//! # Create a legacy adapter project
47//! allframe ignite my-adapter --archetype legacy-adapter
48//! ```
49
50#![deny(missing_docs)]
51
52pub mod config;
53pub mod scaffolding;
54pub mod templates;
55pub mod validation;
56
57pub use config::{Archetype, ProjectConfig};
58
59use std::path::{Path, PathBuf};
60
61use clap::{Parser, Subcommand, ValueEnum};
62
63/// CLI archetype selection (maps to config::Archetype)
64#[derive(Debug, Clone, Copy, Default, ValueEnum)]
65pub enum CliArchetype {
66    /// Basic Clean Architecture project (default)
67    #[default]
68    Basic,
69    /// API Gateway service with gRPC, resilience, and caching
70    Gateway,
71    /// Event consumer service with Kafka, idempotency, and DLQ
72    Consumer,
73    /// Event producer service with outbox pattern and transactional messaging
74    Producer,
75    /// Backend for Frontend API aggregation service
76    Bff,
77    /// Scheduled jobs service with cron-based task execution
78    Scheduled,
79    /// WebSocket gateway for real-time bidirectional communication
80    WebsocketGateway,
81    /// Saga orchestrator for distributed transaction coordination
82    SagaOrchestrator,
83    /// Legacy system adapter (anti-corruption layer)
84    LegacyAdapter,
85}
86
87impl From<CliArchetype> for Archetype {
88    fn from(cli: CliArchetype) -> Self {
89        match cli {
90            CliArchetype::Basic => Archetype::Basic,
91            CliArchetype::Gateway => Archetype::Gateway,
92            CliArchetype::Consumer => Archetype::Consumer,
93            CliArchetype::Producer => Archetype::Producer,
94            CliArchetype::Bff => Archetype::Bff,
95            CliArchetype::Scheduled => Archetype::Scheduled,
96            CliArchetype::WebsocketGateway => Archetype::WebSocketGateway,
97            CliArchetype::SagaOrchestrator => Archetype::SagaOrchestrator,
98            CliArchetype::LegacyAdapter => Archetype::AntiCorruptionLayer,
99        }
100    }
101}
102
103#[derive(Parser)]
104#[command(name = "allframe")]
105#[command(about = "AllFrame CLI - The composable Rust API framework", long_about = None)]
106#[command(version)]
107struct Cli {
108    #[command(subcommand)]
109    command: Commands,
110}
111
112#[derive(Subcommand)]
113enum Commands {
114    /// Create a new AllFrame project
115    Ignite {
116        /// Name of the project to create
117        name: PathBuf,
118
119        /// Project archetype (basic, gateway, consumer)
120        #[arg(short, long, value_enum, default_value_t = CliArchetype::Basic)]
121        archetype: CliArchetype,
122
123        /// Service name (e.g., "kraken" for gateway, "order-processor" for consumer)
124        #[arg(long)]
125        service_name: Option<String>,
126
127        /// Base URL for gateway's external API
128        #[arg(long)]
129        api_base_url: Option<String>,
130
131        /// Consumer group ID (for consumer archetype)
132        #[arg(long)]
133        group_id: Option<String>,
134
135        /// Kafka broker addresses (for consumer archetype)
136        #[arg(long)]
137        brokers: Option<String>,
138
139        /// Enable all features
140        #[arg(long)]
141        all_features: bool,
142    },
143    /// Generate code from LLM prompts (coming soon)
144    Forge {
145        /// The prompt for code generation
146        prompt: String,
147    },
148}
149
150/// Run the AllFrame CLI with command-line arguments.
151///
152/// This is the main entry point for the CLI, designed to be called from
153/// both the `allframe-forge` binary and the `allframe` binary wrapper.
154///
155/// # Errors
156/// Returns an error if command parsing fails or if the executed command fails.
157pub fn run() -> anyhow::Result<()> {
158    let cli = Cli::parse();
159
160    match cli.command {
161        Commands::Ignite {
162            name,
163            archetype,
164            service_name,
165            api_base_url,
166            group_id,
167            brokers: _,
168            all_features: _,
169        } => {
170            ignite_project(&name, archetype, service_name, api_base_url, group_id)?;
171        }
172        Commands::Forge { prompt } => {
173            forge_code(&prompt)?;
174        }
175    }
176
177    Ok(())
178}
179
180/// Create a new AllFrame project
181///
182/// This function orchestrates the creation of a new AllFrame project with
183/// Clean Architecture structure.
184fn ignite_project(
185    project_path: &Path,
186    archetype: CliArchetype,
187    service_name: Option<String>,
188    api_base_url: Option<String>,
189    group_id: Option<String>,
190) -> anyhow::Result<()> {
191    let project_name = project_path
192        .file_name()
193        .and_then(|n| n.to_str())
194        .ok_or_else(|| anyhow::anyhow!("Invalid project path"))?;
195
196    validation::validate_project_name(project_name)?;
197
198    if project_path.exists() {
199        anyhow::bail!("Directory already exists: {}", project_path.display());
200    }
201
202    std::fs::create_dir_all(project_path)?;
203
204    // Build project configuration based on archetype
205    let config = match archetype {
206        CliArchetype::Basic => ProjectConfig::new(project_name),
207        CliArchetype::Gateway => {
208            let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Gateway);
209
210            // Configure gateway-specific settings
211            if let Some(gateway) = config.gateway.as_mut() {
212                if let Some(svc_name) = service_name.clone() {
213                    gateway.service_name = svc_name.clone();
214                    gateway.display_name = to_title_case(&svc_name);
215                } else {
216                    // Default to project name
217                    gateway.service_name = project_name.replace('-', "_");
218                    gateway.display_name = to_title_case(project_name);
219                }
220
221                if let Some(url) = api_base_url {
222                    gateway.api_base_url = url;
223                }
224            }
225
226            config
227        }
228        CliArchetype::Consumer => {
229            let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Consumer);
230
231            // Configure consumer-specific settings
232            if let Some(consumer) = config.consumer.as_mut() {
233                if let Some(svc_name) = service_name {
234                    consumer.service_name = svc_name.clone();
235                    consumer.display_name = to_title_case(&svc_name);
236                } else {
237                    // Default to project name
238                    consumer.service_name = project_name.replace('-', "_");
239                    consumer.display_name = to_title_case(project_name);
240                }
241
242                if let Some(gid) = group_id {
243                    consumer.group_id = gid;
244                } else {
245                    consumer.group_id = format!("{}-group", consumer.service_name);
246                }
247            }
248
249            config
250        }
251        CliArchetype::Producer => {
252            let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Producer);
253
254            // Configure producer-specific settings
255            if let Some(producer) = config.producer.as_mut() {
256                if let Some(svc_name) = service_name.clone() {
257                    producer.service_name = svc_name.clone();
258                    producer.display_name = to_title_case(&svc_name);
259                } else {
260                    // Default to project name
261                    producer.service_name = project_name.replace('-', "_");
262                    producer.display_name = to_title_case(project_name);
263                }
264            }
265
266            config
267        }
268        CliArchetype::Bff => {
269            let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Bff);
270
271            // Configure BFF-specific settings
272            if let Some(bff) = config.bff.as_mut() {
273                if let Some(svc_name) = service_name.clone() {
274                    bff.service_name = svc_name.clone();
275                    bff.display_name = to_title_case(&svc_name);
276                } else {
277                    // Default to project name
278                    bff.service_name = project_name.replace('-', "_");
279                    bff.display_name = to_title_case(project_name);
280                }
281
282                if let Some(url) = api_base_url {
283                    if let Some(backend) = bff.backends.first_mut() {
284                        backend.base_url = url;
285                    }
286                }
287            }
288
289            config
290        }
291        CliArchetype::Scheduled => {
292            let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Scheduled);
293
294            // Configure scheduled-specific settings
295            if let Some(scheduled) = config.scheduled.as_mut() {
296                if let Some(svc_name) = service_name.clone() {
297                    scheduled.service_name = svc_name.clone();
298                    scheduled.display_name = to_title_case(&svc_name);
299                } else {
300                    // Default to project name
301                    scheduled.service_name = project_name.replace('-', "_");
302                    scheduled.display_name = to_title_case(project_name);
303                }
304            }
305
306            config
307        }
308        CliArchetype::WebsocketGateway => {
309            let mut config =
310                ProjectConfig::new(project_name).with_archetype(Archetype::WebSocketGateway);
311
312            // Configure websocket-specific settings
313            if let Some(ws) = config.websocket_gateway.as_mut() {
314                if let Some(svc_name) = service_name.clone() {
315                    ws.service_name = svc_name.clone();
316                    ws.display_name = to_title_case(&svc_name);
317                } else {
318                    // Default to project name
319                    ws.service_name = project_name.replace('-', "_");
320                    ws.display_name = to_title_case(project_name);
321                }
322            }
323
324            config
325        }
326        CliArchetype::SagaOrchestrator => {
327            let mut config =
328                ProjectConfig::new(project_name).with_archetype(Archetype::SagaOrchestrator);
329
330            // Configure saga-specific settings
331            if let Some(saga) = config.saga_orchestrator.as_mut() {
332                if let Some(svc_name) = service_name.clone() {
333                    saga.service_name = svc_name.clone();
334                    saga.display_name = to_title_case(&svc_name);
335                } else {
336                    // Default to project name
337                    saga.service_name = project_name.replace('-', "_");
338                    saga.display_name = to_title_case(project_name);
339                }
340            }
341
342            config
343        }
344        CliArchetype::LegacyAdapter => {
345            let mut config =
346                ProjectConfig::new(project_name).with_archetype(Archetype::AntiCorruptionLayer);
347
348            // Configure legacy adapter settings
349            if let Some(acl) = config.acl.as_mut() {
350                if let Some(svc_name) = service_name {
351                    acl.service_name = svc_name.clone();
352                    acl.display_name = to_title_case(&svc_name);
353                } else {
354                    // Default to project name
355                    acl.service_name = project_name.replace('-', "_");
356                    acl.display_name = to_title_case(project_name);
357                }
358
359                if let Some(url) = api_base_url {
360                    acl.legacy_system.connection_string = url;
361                }
362            }
363
364            config
365        }
366    };
367
368    // Create directory structure and generate files based on archetype
369    match config.archetype {
370        Archetype::Basic => {
371            scaffolding::create_directory_structure(project_path)?;
372            scaffolding::generate_files(project_path, project_name)?;
373        }
374        Archetype::Gateway => {
375            scaffolding::create_gateway_structure(project_path)?;
376            scaffolding::generate_gateway_files(project_path, &config)?;
377        }
378        Archetype::Consumer => {
379            scaffolding::create_consumer_structure(project_path)?;
380            scaffolding::generate_consumer_files(project_path, &config)?;
381        }
382        Archetype::Producer => {
383            scaffolding::create_producer_structure(project_path)?;
384            scaffolding::generate_producer_files(project_path, &config)?;
385        }
386        Archetype::Bff => {
387            scaffolding::create_bff_structure(project_path)?;
388            scaffolding::generate_bff_files(project_path, &config)?;
389        }
390        Archetype::Scheduled => {
391            scaffolding::create_scheduled_structure(project_path)?;
392            scaffolding::generate_scheduled_files(project_path, &config)?;
393        }
394        Archetype::WebSocketGateway => {
395            scaffolding::create_websocket_structure(project_path)?;
396            scaffolding::generate_websocket_files(project_path, &config)?;
397        }
398        Archetype::SagaOrchestrator => {
399            scaffolding::create_saga_structure(project_path)?;
400            scaffolding::generate_saga_files(project_path, &config)?;
401        }
402        Archetype::AntiCorruptionLayer => {
403            scaffolding::create_acl_structure(project_path)?;
404            scaffolding::generate_acl_files(project_path, &config)?;
405        }
406        _ => {
407            anyhow::bail!("Archetype {:?} is not yet implemented", config.archetype);
408        }
409    }
410
411    println!(
412        "AllFrame {} project created successfully: {}",
413        config.archetype, project_name
414    );
415    println!("\nNext steps:");
416    println!("  cd {}", project_name);
417
418    match config.archetype {
419        Archetype::Gateway => {
420            println!("  # Edit src/config.rs to set your API credentials");
421            println!("  cargo build");
422            println!("  cargo run");
423        }
424        Archetype::Consumer => {
425            println!("  # Set environment variables for broker connection");
426            println!("  # See README.md for configuration options");
427            println!("  cargo build");
428            println!("  cargo run");
429        }
430        Archetype::Producer => {
431            println!("  # Set DATABASE_URL and KAFKA_BROKERS environment variables");
432            println!("  # Run database migrations (see README.md)");
433            println!("  cargo build");
434            println!("  cargo run");
435        }
436        Archetype::Bff => {
437            println!("  # Set API_BASE_URL for backend service connection");
438            println!("  # See README.md for configuration options");
439            println!("  cargo build");
440            println!("  cargo run");
441        }
442        Archetype::Scheduled => {
443            println!("  # Configure jobs in src/config.rs");
444            println!("  # See README.md for cron expression reference");
445            println!("  cargo build");
446            println!("  cargo run");
447        }
448        Archetype::WebSocketGateway => {
449            println!("  # Configure channels in src/config.rs");
450            println!("  # See README.md for WebSocket API documentation");
451            println!("  cargo build");
452            println!("  cargo run");
453        }
454        Archetype::SagaOrchestrator => {
455            println!("  # Configure sagas and steps in src/config.rs");
456            println!("  # See README.md for saga pattern documentation");
457            println!("  cargo build");
458            println!("  cargo run");
459        }
460        Archetype::AntiCorruptionLayer => {
461            println!("  # Configure legacy system connection in src/config.rs");
462            println!("  # See README.md for transformer implementation guide");
463            println!("  cargo build");
464            println!("  cargo run");
465        }
466        _ => {
467            println!("  cargo test");
468            println!("  cargo run");
469        }
470    }
471
472    Ok(())
473}
474
475/// Convert a string to title case (e.g., "kraken" -> "Kraken")
476fn to_title_case(s: &str) -> String {
477    s.split(|c| c == '-' || c == '_')
478        .map(|word| {
479            let mut chars = word.chars();
480            match chars.next() {
481                None => String::new(),
482                Some(first) => first.to_uppercase().chain(chars).collect(),
483            }
484        })
485        .collect::<Vec<_>>()
486        .join(" ")
487}
488
489/// Generate code from LLM prompts (not yet implemented)
490fn forge_code(_prompt: &str) -> anyhow::Result<()> {
491    anyhow::bail!("allframe forge is not yet implemented")
492}