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 #[arg(long)]
191 brokers: Option<String>,
192
193 #[arg(long)]
195 all_features: bool,
196 },
197 Saga {
199 #[command(subcommand)]
200 command: SagaCommands,
201 },
202 Forge {
204 prompt: String,
206 },
207}
208
209pub fn run() -> anyhow::Result<()> {
217 let cli = Cli::parse();
218
219 match cli.command {
220 Commands::Ignite {
221 name,
222 archetype,
223 service_name,
224 api_base_url,
225 group_id,
226 brokers: _,
227 all_features: _,
228 } => {
229 ignite_project(&name, archetype, service_name, api_base_url, group_id)?;
230 }
231 Commands::Saga { command } => {
232 handle_saga_command(command)?;
233 }
234 Commands::Forge { prompt } => {
235 forge_code(&prompt)?;
236 }
237 }
238
239 Ok(())
240}
241
242fn ignite_project(
247 project_path: &Path,
248 archetype: CliArchetype,
249 service_name: Option<String>,
250 api_base_url: Option<String>,
251 group_id: Option<String>,
252) -> anyhow::Result<()> {
253 let project_name = project_path
254 .file_name()
255 .and_then(|n| n.to_str())
256 .ok_or_else(|| anyhow::anyhow!("Invalid project path"))?;
257
258 validation::validate_project_name(project_name)?;
259
260 if project_path.exists() {
261 anyhow::bail!("Directory already exists: {}", project_path.display());
262 }
263
264 std::fs::create_dir_all(project_path)?;
265
266 let config = match archetype {
268 CliArchetype::Basic => ProjectConfig::new(project_name),
269 CliArchetype::Gateway => {
270 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Gateway);
271
272 if let Some(gateway) = config.gateway.as_mut() {
274 if let Some(svc_name) = service_name.clone() {
275 gateway.service_name = svc_name.clone();
276 gateway.display_name = to_title_case(&svc_name);
277 } else {
278 gateway.service_name = project_name.replace('-', "_");
280 gateway.display_name = to_title_case(project_name);
281 }
282
283 if let Some(url) = api_base_url {
284 gateway.api_base_url = url;
285 }
286 }
287
288 config
289 }
290 CliArchetype::Consumer => {
291 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Consumer);
292
293 if let Some(consumer) = config.consumer.as_mut() {
295 if let Some(svc_name) = service_name {
296 consumer.service_name = svc_name.clone();
297 consumer.display_name = to_title_case(&svc_name);
298 } else {
299 consumer.service_name = project_name.replace('-', "_");
301 consumer.display_name = to_title_case(project_name);
302 }
303
304 if let Some(gid) = group_id {
305 consumer.group_id = gid;
306 } else {
307 consumer.group_id = format!("{}-group", consumer.service_name);
308 }
309 }
310
311 config
312 }
313 CliArchetype::Producer => {
314 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Producer);
315
316 if let Some(producer) = config.producer.as_mut() {
318 if let Some(svc_name) = service_name.clone() {
319 producer.service_name = svc_name.clone();
320 producer.display_name = to_title_case(&svc_name);
321 } else {
322 producer.service_name = project_name.replace('-', "_");
324 producer.display_name = to_title_case(project_name);
325 }
326 }
327
328 config
329 }
330 CliArchetype::Bff => {
331 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Bff);
332
333 if let Some(bff) = config.bff.as_mut() {
335 if let Some(svc_name) = service_name.clone() {
336 bff.service_name = svc_name.clone();
337 bff.display_name = to_title_case(&svc_name);
338 } else {
339 bff.service_name = project_name.replace('-', "_");
341 bff.display_name = to_title_case(project_name);
342 }
343
344 if let Some(url) = api_base_url {
345 if let Some(backend) = bff.backends.first_mut() {
346 backend.base_url = url;
347 }
348 }
349 }
350
351 config
352 }
353 CliArchetype::Scheduled => {
354 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Scheduled);
355
356 if let Some(scheduled) = config.scheduled.as_mut() {
358 if let Some(svc_name) = service_name.clone() {
359 scheduled.service_name = svc_name.clone();
360 scheduled.display_name = to_title_case(&svc_name);
361 } else {
362 scheduled.service_name = project_name.replace('-', "_");
364 scheduled.display_name = to_title_case(project_name);
365 }
366 }
367
368 config
369 }
370 CliArchetype::WebsocketGateway => {
371 let mut config =
372 ProjectConfig::new(project_name).with_archetype(Archetype::WebSocketGateway);
373
374 if let Some(ws) = config.websocket_gateway.as_mut() {
376 if let Some(svc_name) = service_name.clone() {
377 ws.service_name = svc_name.clone();
378 ws.display_name = to_title_case(&svc_name);
379 } else {
380 ws.service_name = project_name.replace('-', "_");
382 ws.display_name = to_title_case(project_name);
383 }
384 }
385
386 config
387 }
388 CliArchetype::SagaOrchestrator => {
389 let mut config =
390 ProjectConfig::new(project_name).with_archetype(Archetype::SagaOrchestrator);
391
392 if let Some(saga) = config.saga_orchestrator.as_mut() {
394 if let Some(svc_name) = service_name.clone() {
395 saga.service_name = svc_name.clone();
396 saga.display_name = to_title_case(&svc_name);
397 } else {
398 saga.service_name = project_name.replace('-', "_");
400 saga.display_name = to_title_case(project_name);
401 }
402 }
403
404 config
405 }
406 CliArchetype::LegacyAdapter => {
407 let mut config =
408 ProjectConfig::new(project_name).with_archetype(Archetype::AntiCorruptionLayer);
409
410 if let Some(acl) = config.acl.as_mut() {
412 if let Some(svc_name) = service_name {
413 acl.service_name = svc_name.clone();
414 acl.display_name = to_title_case(&svc_name);
415 } else {
416 acl.service_name = project_name.replace('-', "_");
418 acl.display_name = to_title_case(project_name);
419 }
420
421 if let Some(url) = api_base_url {
422 acl.legacy_system.connection_string = url;
423 }
424 }
425
426 config
427 }
428 };
429
430 match config.archetype {
432 Archetype::Basic => {
433 scaffolding::create_directory_structure(project_path)?;
434 scaffolding::generate_files(project_path, project_name)?;
435 }
436 Archetype::Gateway => {
437 scaffolding::create_gateway_structure(project_path)?;
438 scaffolding::generate_gateway_files(project_path, &config)?;
439 }
440 Archetype::Consumer => {
441 scaffolding::create_consumer_structure(project_path)?;
442 scaffolding::generate_consumer_files(project_path, &config)?;
443 }
444 Archetype::Producer => {
445 scaffolding::create_producer_structure(project_path)?;
446 scaffolding::generate_producer_files(project_path, &config)?;
447 }
448 Archetype::Bff => {
449 scaffolding::create_bff_structure(project_path)?;
450 scaffolding::generate_bff_files(project_path, &config)?;
451 }
452 Archetype::Scheduled => {
453 scaffolding::create_scheduled_structure(project_path)?;
454 scaffolding::generate_scheduled_files(project_path, &config)?;
455 }
456 Archetype::WebSocketGateway => {
457 scaffolding::create_websocket_structure(project_path)?;
458 scaffolding::generate_websocket_files(project_path, &config)?;
459 }
460 Archetype::SagaOrchestrator => {
461 scaffolding::create_saga_structure(project_path)?;
462 scaffolding::generate_saga_files(project_path, &config)?;
463 }
464 Archetype::AntiCorruptionLayer => {
465 scaffolding::create_acl_structure(project_path)?;
466 scaffolding::generate_acl_files(project_path, &config)?;
467 }
468 _ => {
469 anyhow::bail!("Archetype {:?} is not yet implemented", config.archetype);
470 }
471 }
472
473 println!(
474 "AllFrame {} project created successfully: {}",
475 config.archetype, project_name
476 );
477 println!("\nNext steps:");
478 println!(" cd {}", project_name);
479
480 match config.archetype {
481 Archetype::Gateway => {
482 println!(" # Edit src/config.rs to set your API credentials");
483 println!(" cargo build");
484 println!(" cargo run");
485 }
486 Archetype::Consumer => {
487 println!(" # Set environment variables for broker connection");
488 println!(" # See README.md for configuration options");
489 println!(" cargo build");
490 println!(" cargo run");
491 }
492 Archetype::Producer => {
493 println!(" # Set DATABASE_URL and KAFKA_BROKERS environment variables");
494 println!(" # Run database migrations (see README.md)");
495 println!(" cargo build");
496 println!(" cargo run");
497 }
498 Archetype::Bff => {
499 println!(" # Set API_BASE_URL for backend service connection");
500 println!(" # See README.md for configuration options");
501 println!(" cargo build");
502 println!(" cargo run");
503 }
504 Archetype::Scheduled => {
505 println!(" # Configure jobs in src/config.rs");
506 println!(" # See README.md for cron expression reference");
507 println!(" cargo build");
508 println!(" cargo run");
509 }
510 Archetype::WebSocketGateway => {
511 println!(" # Configure channels in src/config.rs");
512 println!(" # See README.md for WebSocket API documentation");
513 println!(" cargo build");
514 println!(" cargo run");
515 }
516 Archetype::SagaOrchestrator => {
517 println!(" # Configure sagas and steps in src/config.rs");
518 println!(" # See README.md for saga pattern documentation");
519 println!(" cargo build");
520 println!(" cargo run");
521 }
522 Archetype::AntiCorruptionLayer => {
523 println!(" # Configure legacy system connection in src/config.rs");
524 println!(" # See README.md for transformer implementation guide");
525 println!(" cargo build");
526 println!(" cargo run");
527 }
528 _ => {
529 println!(" cargo test");
530 println!(" cargo run");
531 }
532 }
533
534 Ok(())
535}
536
537fn to_title_case(s: &str) -> String {
539 s.split(|c| c == '-' || c == '_')
540 .map(|word| {
541 let mut chars = word.chars();
542 match chars.next() {
543 None => String::new(),
544 Some(first) => first.to_uppercase().chain(chars).collect(),
545 }
546 })
547 .collect::<Vec<_>>()
548 .join(" ")
549}
550
551fn handle_saga_command(command: SagaCommands) -> anyhow::Result<()> {
553 match command {
554 SagaCommands::New { name, steps, path } => {
555 saga_new(&name, &steps, &path)?;
556 }
557 SagaCommands::AddStep {
558 saga,
559 name,
560 position,
561 timeout,
562 requires_compensation,
563 path,
564 } => {
565 saga_add_step(
566 &saga,
567 &name,
568 &position,
569 timeout,
570 requires_compensation,
571 &path,
572 )?;
573 }
574 SagaCommands::Validate { name, path } => {
575 saga_validate(&name, &path)?;
576 }
577 }
578 Ok(())
579}
580
581fn saga_new(name: &str, steps: &str, base_path: &Path) -> anyhow::Result<()> {
583 println!("Creating saga '{}' with steps: {}", name, steps);
584 println!("Base path: {}", base_path.display());
585
586 let step_list: Vec<&str> = steps.split(',').map(|s| s.trim()).collect();
588
589 let saga_path = base_path.join(name.to_lowercase());
591 std::fs::create_dir_all(&saga_path)?;
592
593 generate_saga_files(&saga_path, name, &step_list)?;
595
596 println!("Saga '{}' created successfully!", name);
597 Ok(())
598}
599
600fn saga_add_step(
602 saga: &str,
603 step_name: &str,
604 position: &str,
605 timeout: u64,
606 requires_compensation: bool,
607 base_path: &Path,
608) -> anyhow::Result<()> {
609 println!(
610 "Adding step '{}' to saga '{}' at position '{}'",
611 step_name, saga, position
612 );
613 println!(
614 "Timeout: {}s, Requires compensation: {}",
615 timeout, requires_compensation
616 );
617
618 let saga_path = base_path.join(saga.to_lowercase());
619 if !saga_path.exists() {
620 anyhow::bail!("Saga '{}' not found at {}", saga, saga_path.display());
621 }
622
623 generate_step_file(&saga_path, step_name, timeout, requires_compensation)?;
625
626 println!(
627 "Step '{}' added to saga '{}' successfully!",
628 step_name, saga
629 );
630 Ok(())
631}
632
633fn saga_validate(name: &str, base_path: &Path) -> anyhow::Result<()> {
635 println!("Validating saga '{}'...", name);
636
637 let saga_path = base_path.join(name.to_lowercase());
638 if !saga_path.exists() {
639 anyhow::bail!("Saga '{}' not found at {}", name, saga_path.display());
640 }
641
642 let mod_file = saga_path.join("mod.rs");
644 let saga_file = saga_path.join(format!("{}.rs", name.to_lowercase()));
645
646 if !mod_file.exists() {
647 println!("⚠️ Missing mod.rs file");
648 } else {
649 println!("✅ mod.rs found");
650 }
651
652 if !saga_file.exists() {
653 println!("⚠️ Missing saga implementation file");
654 } else {
655 println!("✅ Saga implementation file found");
656 }
657
658 println!("Saga '{}' validation completed!", name);
659 Ok(())
660}
661
662fn generate_saga_files(saga_path: &Path, name: &str, steps: &[&str]) -> anyhow::Result<()> {
664 let step_mods = steps
666 .iter()
667 .map(|step| format!("pub mod {};", step.to_lowercase()))
668 .collect::<Vec<_>>()
669 .join("\n");
670
671 let mod_content = format!(
672 "//! {} saga implementation
673//!
674//! This module contains the {} saga and its associated steps.
675
676pub mod {};
677{}",
678 name,
679 name,
680 name.to_lowercase(),
681 step_mods
682 );
683 std::fs::write(saga_path.join("mod.rs"), mod_content)?;
684
685 let saga_file_name = format!("{}.rs", name.to_lowercase());
687 let saga_content = generate_saga_content(name, steps);
688 std::fs::write(saga_path.join(saga_file_name), saga_content)?;
689
690 for step in steps {
692 generate_step_file(saga_path, step, 30, true)?;
693 }
694
695 Ok(())
696}
697
698fn generate_saga_content(name: &str, steps: &[&str]) -> String {
700 let data_struct_name = format!("{}Data", name);
701 let _workflow_enum_name = format!("{}Workflow", name);
702
703 let step_imports = steps
704 .iter()
705 .map(|step| format!("use super::{};", step.to_lowercase()))
706 .collect::<Vec<_>>()
707 .join("\n");
708
709 let workflow_variants = steps
710 .iter()
711 .enumerate()
712 .map(|(i, step)| format!(" /// Step {}: {}\n {},", i + 1, step, step))
713 .collect::<Vec<_>>()
714 .join("\n");
715
716 format!(
717 "//! {} Saga Implementation
718//!
719//! This file contains the {} saga implementation using AllFrame macros.
720
721use std::sync::Arc;
722use serde::{{Deserialize, Serialize}};
723use allframe_core::cqrs::{{Saga, MacroSagaStep}};
724
725{}
726
727// Saga data structure
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct {} {{
730 pub user_id: String,
731 // Add saga-specific data fields here
732}}
733
734// Saga implementation
735#[allframe_macros::saga(name = \"{}\", data_field = \"data\")]
736pub struct {} {{
737 #[saga_data]
738 data: {},
739
740 // Add dependency injections here
741 // #[inject] repository: Arc<dyn SomeRepository>,
742}}
743
744// Saga workflow
745#[allframe_macros::saga_workflow({})]
746pub enum {} {{
747{}
748}}
749
750// Step constructor implementations
751impl {} {{
752{}
753}}
754",
755 name,
756 name,
757 step_imports,
758 data_struct_name,
759 name,
760 name,
761 data_struct_name,
762 name,
763 name,
764 workflow_variants,
765 name,
766 steps
767 .iter()
768 .map(|step| {
769 let step_struct = format!("{}Step", step);
770 let constructor = format!("create_{}_step", step.to_lowercase());
771 format!(
772 " pub fn {}(&self) -> Arc<dyn MacroSagaStep> {{
773 Arc::new({} {{
774 // TODO: Initialize step with saga dependencies
775 user_id: self.data.user_id.clone(),
776 // Add other dependencies from saga fields
777 }})
778 }}",
779 constructor, step_struct
780 )
781 })
782 .collect::<Vec<_>>()
783 .join("\n\n")
784 )
785}
786
787fn generate_step_file(
789 saga_path: &Path,
790 step_name: &str,
791 _timeout: u64,
792 requires_compensation: bool,
793) -> anyhow::Result<()> {
794 let file_name = format!("{}.rs", step_name.to_lowercase());
795 let struct_name = format!("{}Step", step_name);
796
797 let compensation_attr = if requires_compensation {
798 ""
799 } else {
800 ", requires_compensation = false"
801 };
802
803 let content = format!(
804 "//! {} Step Implementation
805
806use std::sync::Arc;
807use serde::{{Deserialize, Serialize}};
808use allframe_core::cqrs::{{MacroSagaStep, SagaContext, StepExecutionResult}};
809
810#[derive(Serialize, Deserialize)]
811pub struct {}Output {{
812 // Define step output fields here
813 pub success: bool,
814}}
815
816// Step implementation
817#[allframe_macros::saga_step(name = \"{}\"{}))]
818pub struct {} {{
819 pub user_id: String,
820 // Add step-specific fields and injected dependencies here
821}}
822
823impl {} {{
824 pub fn new(user_id: String) -> Self {{
825 Self {{ user_id }}
826 }}
827
828 async fn execute(&self, _ctx: &SagaContext) -> StepExecutionResult {{
829 // TODO: Implement step execution logic
830 println!(\"Executing step: {}\");
831
832 // Return success with output
833 {}Output {{
834 success: true,
835 }}.into()
836 }}
837}}
838",
839 step_name,
840 step_name,
841 step_name,
842 compensation_attr,
843 struct_name,
844 struct_name,
845 step_name,
846 step_name
847 );
848
849 std::fs::write(saga_path.join(file_name), content)?;
850 Ok(())
851}
852
853fn forge_code(_prompt: &str) -> anyhow::Result<()> {
855 anyhow::bail!("allframe forge is not yet implemented")
856}