1#![deny(missing_docs)]
54
55pub mod config;
56pub mod scaffolding;
57pub mod templates;
58pub mod validation;
59
60use std::path::{Path, PathBuf};
61
62use clap::{Parser, Subcommand, ValueEnum};
63pub use config::{Archetype, ProjectConfig};
64
65#[derive(Debug, Clone, Copy, Default, ValueEnum)]
67pub enum CliArchetype {
68 #[default]
70 Basic,
71 Gateway,
73 Consumer,
75 Producer,
77 Bff,
79 Scheduled,
81 WebsocketGateway,
83 SagaOrchestrator,
85 LegacyAdapter,
87}
88
89impl From<CliArchetype> for Archetype {
90 fn from(cli: CliArchetype) -> Self {
91 match cli {
92 CliArchetype::Basic => Archetype::Basic,
93 CliArchetype::Gateway => Archetype::Gateway,
94 CliArchetype::Consumer => Archetype::Consumer,
95 CliArchetype::Producer => Archetype::Producer,
96 CliArchetype::Bff => Archetype::Bff,
97 CliArchetype::Scheduled => Archetype::Scheduled,
98 CliArchetype::WebsocketGateway => Archetype::WebSocketGateway,
99 CliArchetype::SagaOrchestrator => Archetype::SagaOrchestrator,
100 CliArchetype::LegacyAdapter => Archetype::AntiCorruptionLayer,
101 }
102 }
103}
104
105#[derive(Subcommand)]
106enum SagaCommands {
107 New {
109 name: String,
111
112 #[arg(short, long)]
114 steps: String,
115
116 #[arg(long, default_value = "src/application/cqrs/sagas")]
118 path: PathBuf,
119 },
120 AddStep {
122 saga: String,
124
125 name: String,
127
128 #[arg(short, long, default_value = "last")]
131 position: String,
132
133 #[arg(short, long, default_value = "30")]
135 timeout: u64,
136
137 #[arg(long)]
139 requires_compensation: bool,
140
141 #[arg(long, default_value = "src/application/cqrs/sagas")]
143 path: PathBuf,
144 },
145 Validate {
147 name: String,
149
150 #[arg(long, default_value = "src/application/cqrs/sagas")]
152 path: PathBuf,
153 },
154}
155
156#[derive(Parser)]
157#[command(name = "allframe")]
158#[command(about = "AllFrame CLI - The composable Rust API framework", long_about = None)]
159#[command(version)]
160struct Cli {
161 #[command(subcommand)]
162 command: Commands,
163}
164
165#[derive(Subcommand)]
166enum Commands {
167 Ignite {
169 name: PathBuf,
171
172 #[arg(short, long, value_enum, default_value_t = CliArchetype::Basic)]
174 archetype: CliArchetype,
175
176 #[arg(long)]
179 service_name: Option<String>,
180
181 #[arg(long)]
183 api_base_url: Option<String>,
184
185 #[arg(long)]
187 group_id: Option<String>,
188
189 },
190 Saga {
192 #[command(subcommand)]
193 command: SagaCommands,
194 },
195}
196
197pub fn run() -> anyhow::Result<()> {
205 let cli = Cli::parse();
206
207 match cli.command {
208 Commands::Ignite {
209 name,
210 archetype,
211 service_name,
212 api_base_url,
213 group_id,
214 } => {
215 ignite_project(&name, archetype, service_name, api_base_url, group_id)?;
216 }
217 Commands::Saga { command } => {
218 handle_saga_command(command)?;
219 }
220 }
221
222 Ok(())
223}
224
225fn ignite_project(
230 project_path: &Path,
231 archetype: CliArchetype,
232 service_name: Option<String>,
233 api_base_url: Option<String>,
234 group_id: Option<String>,
235) -> anyhow::Result<()> {
236 let project_name = project_path
237 .file_name()
238 .and_then(|n| n.to_str())
239 .ok_or_else(|| anyhow::anyhow!("Invalid project path"))?;
240
241 validation::validate_project_name(project_name)?;
242
243 if project_path.exists() {
244 anyhow::bail!("Directory already exists: {}", project_path.display());
245 }
246
247 std::fs::create_dir_all(project_path)?;
248
249 let config = match archetype {
251 CliArchetype::Basic => ProjectConfig::new(project_name),
252 CliArchetype::Gateway => {
253 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Gateway);
254
255 if let Some(gateway) = config.gateway.as_mut() {
257 if let Some(svc_name) = service_name.clone() {
258 gateway.service_name = svc_name.clone();
259 gateway.display_name = to_title_case(&svc_name);
260 } else {
261 gateway.service_name = project_name.replace('-', "_");
263 gateway.display_name = to_title_case(project_name);
264 }
265
266 if let Some(url) = api_base_url {
267 gateway.api_base_url = url;
268 }
269 }
270
271 config
272 }
273 CliArchetype::Consumer => {
274 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Consumer);
275
276 if let Some(consumer) = config.consumer.as_mut() {
278 if let Some(svc_name) = service_name {
279 consumer.service_name = svc_name.clone();
280 consumer.display_name = to_title_case(&svc_name);
281 } else {
282 consumer.service_name = project_name.replace('-', "_");
284 consumer.display_name = to_title_case(project_name);
285 }
286
287 if let Some(gid) = group_id {
288 consumer.group_id = gid;
289 } else {
290 consumer.group_id = format!("{}-group", consumer.service_name);
291 }
292 }
293
294 config
295 }
296 CliArchetype::Producer => {
297 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Producer);
298
299 if let Some(producer) = config.producer.as_mut() {
301 if let Some(svc_name) = service_name.clone() {
302 producer.service_name = svc_name.clone();
303 producer.display_name = to_title_case(&svc_name);
304 } else {
305 producer.service_name = project_name.replace('-', "_");
307 producer.display_name = to_title_case(project_name);
308 }
309 }
310
311 config
312 }
313 CliArchetype::Bff => {
314 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Bff);
315
316 if let Some(bff) = config.bff.as_mut() {
318 if let Some(svc_name) = service_name.clone() {
319 bff.service_name = svc_name.clone();
320 bff.display_name = to_title_case(&svc_name);
321 } else {
322 bff.service_name = project_name.replace('-', "_");
324 bff.display_name = to_title_case(project_name);
325 }
326
327 if let Some(url) = api_base_url {
328 if let Some(backend) = bff.backends.first_mut() {
329 backend.base_url = url;
330 }
331 }
332 }
333
334 config
335 }
336 CliArchetype::Scheduled => {
337 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Scheduled);
338
339 if let Some(scheduled) = config.scheduled.as_mut() {
341 if let Some(svc_name) = service_name.clone() {
342 scheduled.service_name = svc_name.clone();
343 scheduled.display_name = to_title_case(&svc_name);
344 } else {
345 scheduled.service_name = project_name.replace('-', "_");
347 scheduled.display_name = to_title_case(project_name);
348 }
349 }
350
351 config
352 }
353 CliArchetype::WebsocketGateway => {
354 let mut config =
355 ProjectConfig::new(project_name).with_archetype(Archetype::WebSocketGateway);
356
357 if let Some(ws) = config.websocket_gateway.as_mut() {
359 if let Some(svc_name) = service_name.clone() {
360 ws.service_name = svc_name.clone();
361 ws.display_name = to_title_case(&svc_name);
362 } else {
363 ws.service_name = project_name.replace('-', "_");
365 ws.display_name = to_title_case(project_name);
366 }
367 }
368
369 config
370 }
371 CliArchetype::SagaOrchestrator => {
372 let mut config =
373 ProjectConfig::new(project_name).with_archetype(Archetype::SagaOrchestrator);
374
375 if let Some(saga) = config.saga_orchestrator.as_mut() {
377 if let Some(svc_name) = service_name.clone() {
378 saga.service_name = svc_name.clone();
379 saga.display_name = to_title_case(&svc_name);
380 } else {
381 saga.service_name = project_name.replace('-', "_");
383 saga.display_name = to_title_case(project_name);
384 }
385 }
386
387 config
388 }
389 CliArchetype::LegacyAdapter => {
390 let mut config =
391 ProjectConfig::new(project_name).with_archetype(Archetype::AntiCorruptionLayer);
392
393 if let Some(acl) = config.acl.as_mut() {
395 if let Some(svc_name) = service_name {
396 acl.service_name = svc_name.clone();
397 acl.display_name = to_title_case(&svc_name);
398 } else {
399 acl.service_name = project_name.replace('-', "_");
401 acl.display_name = to_title_case(project_name);
402 }
403
404 if let Some(url) = api_base_url {
405 acl.legacy_system.connection_string = url;
406 }
407 }
408
409 config
410 }
411 };
412
413 match config.archetype {
415 Archetype::Basic => {
416 scaffolding::create_directory_structure(project_path)?;
417 scaffolding::generate_files(project_path, project_name)?;
418 }
419 Archetype::Gateway => {
420 scaffolding::create_gateway_structure(project_path)?;
421 scaffolding::generate_gateway_files(project_path, &config)?;
422 }
423 Archetype::Consumer => {
424 scaffolding::create_consumer_structure(project_path)?;
425 scaffolding::generate_consumer_files(project_path, &config)?;
426 }
427 Archetype::Producer => {
428 scaffolding::create_producer_structure(project_path)?;
429 scaffolding::generate_producer_files(project_path, &config)?;
430 }
431 Archetype::Bff => {
432 scaffolding::create_bff_structure(project_path)?;
433 scaffolding::generate_bff_files(project_path, &config)?;
434 }
435 Archetype::Scheduled => {
436 scaffolding::create_scheduled_structure(project_path)?;
437 scaffolding::generate_scheduled_files(project_path, &config)?;
438 }
439 Archetype::WebSocketGateway => {
440 scaffolding::create_websocket_structure(project_path)?;
441 scaffolding::generate_websocket_files(project_path, &config)?;
442 }
443 Archetype::SagaOrchestrator => {
444 scaffolding::create_saga_structure(project_path)?;
445 scaffolding::generate_saga_files(project_path, &config)?;
446 }
447 Archetype::AntiCorruptionLayer => {
448 scaffolding::create_acl_structure(project_path)?;
449 scaffolding::generate_acl_files(project_path, &config)?;
450 }
451 _ => {
452 anyhow::bail!("Archetype {:?} is not yet implemented", config.archetype);
453 }
454 }
455
456 println!(
457 "AllFrame {} project created successfully: {}",
458 config.archetype, project_name
459 );
460 println!("\nNext steps:");
461 println!(" cd {}", project_name);
462
463 match config.archetype {
464 Archetype::Gateway => {
465 println!(" # Edit src/config.rs to set your API credentials");
466 println!(" cargo build");
467 println!(" cargo run");
468 }
469 Archetype::Consumer => {
470 println!(" # Set environment variables for broker connection");
471 println!(" # See README.md for configuration options");
472 println!(" cargo build");
473 println!(" cargo run");
474 }
475 Archetype::Producer => {
476 println!(" # Set DATABASE_URL and KAFKA_BROKERS environment variables");
477 println!(" # Run database migrations (see README.md)");
478 println!(" cargo build");
479 println!(" cargo run");
480 }
481 Archetype::Bff => {
482 println!(" # Set API_BASE_URL for backend service connection");
483 println!(" # See README.md for configuration options");
484 println!(" cargo build");
485 println!(" cargo run");
486 }
487 Archetype::Scheduled => {
488 println!(" # Configure jobs in src/config.rs");
489 println!(" # See README.md for cron expression reference");
490 println!(" cargo build");
491 println!(" cargo run");
492 }
493 Archetype::WebSocketGateway => {
494 println!(" # Configure channels in src/config.rs");
495 println!(" # See README.md for WebSocket API documentation");
496 println!(" cargo build");
497 println!(" cargo run");
498 }
499 Archetype::SagaOrchestrator => {
500 println!(" # Configure sagas and steps in src/config.rs");
501 println!(" # See README.md for saga pattern documentation");
502 println!(" cargo build");
503 println!(" cargo run");
504 }
505 Archetype::AntiCorruptionLayer => {
506 println!(" # Configure legacy system connection in src/config.rs");
507 println!(" # See README.md for transformer implementation guide");
508 println!(" cargo build");
509 println!(" cargo run");
510 }
511 _ => {
512 println!(" cargo test");
513 println!(" cargo run");
514 }
515 }
516
517 Ok(())
518}
519
520fn to_title_case(s: &str) -> String {
522 s.split(['-', '_'])
523 .map(|word| {
524 let mut chars = word.chars();
525 match chars.next() {
526 None => String::new(),
527 Some(first) => first.to_uppercase().chain(chars).collect(),
528 }
529 })
530 .collect::<Vec<_>>()
531 .join(" ")
532}
533
534fn handle_saga_command(command: SagaCommands) -> anyhow::Result<()> {
536 match command {
537 SagaCommands::New { name, steps, path } => {
538 saga_new(&name, &steps, &path)?;
539 }
540 SagaCommands::AddStep {
541 saga,
542 name,
543 position,
544 timeout,
545 requires_compensation,
546 path,
547 } => {
548 saga_add_step(
549 &saga,
550 &name,
551 &position,
552 timeout,
553 requires_compensation,
554 &path,
555 )?;
556 }
557 SagaCommands::Validate { name, path } => {
558 saga_validate(&name, &path)?;
559 }
560 }
561 Ok(())
562}
563
564fn saga_new(name: &str, steps: &str, base_path: &Path) -> anyhow::Result<()> {
566 println!("Creating saga '{}' with steps: {}", name, steps);
567 println!("Base path: {}", base_path.display());
568
569 let step_list: Vec<&str> = steps.split(',').map(|s| s.trim()).collect();
571
572 let saga_path = base_path.join(name.to_lowercase());
574 std::fs::create_dir_all(&saga_path)?;
575
576 generate_saga_files(&saga_path, name, &step_list)?;
578
579 println!("Saga '{}' created successfully!", name);
580 Ok(())
581}
582
583fn saga_add_step(
585 saga: &str,
586 step_name: &str,
587 position: &str,
588 timeout: u64,
589 requires_compensation: bool,
590 base_path: &Path,
591) -> anyhow::Result<()> {
592 println!(
593 "Adding step '{}' to saga '{}' at position '{}'",
594 step_name, saga, position
595 );
596 println!(
597 "Timeout: {}s, Requires compensation: {}",
598 timeout, requires_compensation
599 );
600
601 let saga_path = base_path.join(saga.to_lowercase());
602 if !saga_path.exists() {
603 anyhow::bail!("Saga '{}' not found at {}", saga, saga_path.display());
604 }
605
606 generate_step_file(&saga_path, step_name, timeout, requires_compensation)?;
608
609 println!(
610 "Step '{}' added to saga '{}' successfully!",
611 step_name, saga
612 );
613 Ok(())
614}
615
616fn saga_validate(name: &str, base_path: &Path) -> anyhow::Result<()> {
618 println!("Validating saga '{}'...", name);
619
620 let saga_path = base_path.join(name.to_lowercase());
621 if !saga_path.exists() {
622 anyhow::bail!("Saga '{}' not found at {}", name, saga_path.display());
623 }
624
625 let mod_file = saga_path.join("mod.rs");
627 let saga_file = saga_path.join(format!("{}.rs", name.to_lowercase()));
628
629 if !mod_file.exists() {
630 println!("⚠️ Missing mod.rs file");
631 } else {
632 println!("✅ mod.rs found");
633 }
634
635 if !saga_file.exists() {
636 println!("⚠️ Missing saga implementation file");
637 } else {
638 println!("✅ Saga implementation file found");
639 }
640
641 println!("Saga '{}' validation completed!", name);
642 Ok(())
643}
644
645fn generate_saga_files(saga_path: &Path, name: &str, steps: &[&str]) -> anyhow::Result<()> {
647 let step_mods = steps
649 .iter()
650 .map(|step| format!("pub mod {};", step.to_lowercase()))
651 .collect::<Vec<_>>()
652 .join("\n");
653
654 let mod_content = format!(
655 "//! {} saga implementation
656//!
657//! This module contains the {} saga and its associated steps.
658
659pub mod {};
660{}",
661 name,
662 name,
663 name.to_lowercase(),
664 step_mods
665 );
666 std::fs::write(saga_path.join("mod.rs"), mod_content)?;
667
668 let saga_file_name = format!("{}.rs", name.to_lowercase());
670 let saga_content = generate_saga_content(name, steps);
671 std::fs::write(saga_path.join(saga_file_name), saga_content)?;
672
673 for step in steps {
675 generate_step_file(saga_path, step, 30, true)?;
676 }
677
678 Ok(())
679}
680
681fn generate_saga_content(name: &str, steps: &[&str]) -> String {
683 let data_struct_name = format!("{}Data", name);
684 let _workflow_enum_name = format!("{}Workflow", name);
685
686 let step_imports = steps
687 .iter()
688 .map(|step| format!("use super::{};", step.to_lowercase()))
689 .collect::<Vec<_>>()
690 .join("\n");
691
692 let workflow_variants = steps
693 .iter()
694 .enumerate()
695 .map(|(i, step)| format!(" /// Step {}: {}\n {},", i + 1, step, step))
696 .collect::<Vec<_>>()
697 .join("\n");
698
699 format!(
700 "//! {} Saga Implementation
701//!
702//! This file contains the {} saga implementation using AllFrame macros.
703
704use std::sync::Arc;
705use serde::{{Deserialize, Serialize}};
706use allframe_core::cqrs::{{Saga, MacroSagaStep}};
707
708{}
709
710// Saga data structure
711#[derive(Debug, Clone, Serialize, Deserialize)]
712pub struct {} {{
713 pub user_id: String,
714 // Add saga-specific data fields here
715}}
716
717// Saga implementation
718#[allframe_macros::saga(name = \"{}\", data_field = \"data\")]
719pub struct {} {{
720 #[saga_data]
721 data: {},
722
723 // Add dependency injections here
724 // #[inject] repository: Arc<dyn SomeRepository>,
725}}
726
727// Saga workflow
728#[allframe_macros::saga_workflow({})]
729pub enum {} {{
730{}
731}}
732
733// Step constructor implementations
734impl {} {{
735{}
736}}
737",
738 name,
739 name,
740 step_imports,
741 data_struct_name,
742 name,
743 name,
744 data_struct_name,
745 name,
746 name,
747 workflow_variants,
748 name,
749 steps
750 .iter()
751 .map(|step| {
752 let step_struct = format!("{}Step", step);
753 let constructor = format!("create_{}_step", step.to_lowercase());
754 format!(
755 " pub fn {}(&self) -> Arc<dyn MacroSagaStep> {{
756 Arc::new({} {{
757 // TODO: Initialize step with saga dependencies
758 user_id: self.data.user_id.clone(),
759 // Add other dependencies from saga fields
760 }})
761 }}",
762 constructor, step_struct
763 )
764 })
765 .collect::<Vec<_>>()
766 .join("\n\n")
767 )
768}
769
770fn generate_step_file(
772 saga_path: &Path,
773 step_name: &str,
774 _timeout: u64,
775 requires_compensation: bool,
776) -> anyhow::Result<()> {
777 let file_name = format!("{}.rs", step_name.to_lowercase());
778 let struct_name = format!("{}Step", step_name);
779
780 let compensation_attr = if requires_compensation {
781 ""
782 } else {
783 ", requires_compensation = false"
784 };
785
786 let content = format!(
787 "//! {} Step Implementation
788
789use std::sync::Arc;
790use serde::{{Deserialize, Serialize}};
791use allframe_core::cqrs::{{MacroSagaStep, SagaContext, StepExecutionResult}};
792
793#[derive(Serialize, Deserialize)]
794pub struct {}Output {{
795 // Define step output fields here
796 pub success: bool,
797}}
798
799// Step implementation
800#[allframe_macros::saga_step(name = \"{}\"{}))]
801pub struct {} {{
802 pub user_id: String,
803 // Add step-specific fields and injected dependencies here
804}}
805
806impl {} {{
807 pub fn new(user_id: String) -> Self {{
808 Self {{ user_id }}
809 }}
810
811 async fn execute(&self, _ctx: &SagaContext) -> StepExecutionResult {{
812 // TODO: Implement step execution logic
813 println!(\"Executing step: {}\");
814
815 // Return success with output
816 {}Output {{
817 success: true,
818 }}.into()
819 }}
820}}
821",
822 step_name,
823 step_name,
824 step_name,
825 compensation_attr,
826 struct_name,
827 struct_name,
828 step_name,
829 step_name
830 );
831
832 std::fs::write(saga_path.join(file_name), content)?;
833 Ok(())
834}
835