Skip to main content

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
13//!   messaging
14//! - `bff`: Backend for Frontend API aggregation service
15//! - `scheduled`: Scheduled jobs service with cron-based task execution
16//! - `websocket-gateway`: WebSocket gateway for real-time bidirectional
17//!   communication
18//! - `saga-orchestrator`: Saga orchestrator for distributed transaction
19//!   coordination
20//! - `legacy-adapter`: Legacy system adapter (anti-corruption layer)
21//!
22//! # Usage
23//!
24//! ```bash
25//! # Create a basic project
26//! allframe ignite my-service
27//!
28//! # Create a gateway project
29//! allframe ignite my-gateway --archetype gateway
30//!
31//! # Create a consumer project
32//! allframe ignite my-consumer --archetype consumer
33//!
34//! # Create a producer project
35//! allframe ignite my-producer --archetype producer
36//!
37//! # Create a BFF project
38//! allframe ignite my-bff --archetype bff
39//!
40//! # Create a scheduled jobs project
41//! allframe ignite my-scheduler --archetype scheduled
42//!
43//! # Create a WebSocket gateway project
44//! allframe ignite my-ws --archetype websocket-gateway
45//!
46//! # Create a saga orchestrator project
47//! allframe ignite my-saga --archetype saga-orchestrator
48//!
49//! # Create a legacy adapter project
50//! allframe ignite my-adapter --archetype legacy-adapter
51//! ```
52
53#![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/// CLI archetype selection (maps to config::Archetype)
66#[derive(Debug, Clone, Copy, Default, ValueEnum)]
67pub enum CliArchetype {
68    /// Basic Clean Architecture project (default)
69    #[default]
70    Basic,
71    /// API Gateway service with gRPC, resilience, and caching
72    Gateway,
73    /// Event consumer service with Kafka, idempotency, and DLQ
74    Consumer,
75    /// Event producer service with outbox pattern and transactional messaging
76    Producer,
77    /// Backend for Frontend API aggregation service
78    Bff,
79    /// Scheduled jobs service with cron-based task execution
80    Scheduled,
81    /// WebSocket gateway for real-time bidirectional communication
82    WebsocketGateway,
83    /// Saga orchestrator for distributed transaction coordination
84    SagaOrchestrator,
85    /// Legacy system adapter (anti-corruption layer)
86    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    /// Create a new saga with specified steps
108    New {
109        /// Name of the saga to create
110        name: String,
111
112        /// Comma-separated list of step names
113        #[arg(short, long)]
114        steps: String,
115
116        /// Base path for sagas (default: src/application/cqrs/sagas)
117        #[arg(long, default_value = "src/application/cqrs/sagas")]
118        path: PathBuf,
119    },
120    /// Add a step to an existing saga
121    AddStep {
122        /// Name of the saga to modify
123        saga: String,
124
125        /// Name of the step to add
126        name: String,
127
128        /// Position to insert the step (first, last, after:<step>,
129        /// before:<step>)
130        #[arg(short, long, default_value = "last")]
131        position: String,
132
133        /// Step timeout in seconds
134        #[arg(short, long, default_value = "30")]
135        timeout: u64,
136
137        /// Whether the step requires compensation
138        #[arg(long)]
139        requires_compensation: bool,
140
141        /// Base path for sagas (default: src/application/cqrs/sagas)
142        #[arg(long, default_value = "src/application/cqrs/sagas")]
143        path: PathBuf,
144    },
145    /// Validate saga implementation
146    Validate {
147        /// Name of the saga to validate
148        name: String,
149
150        /// Base path for sagas (default: src/application/cqrs/sagas)
151        #[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    /// Create a new AllFrame project
168    Ignite {
169        /// Name of the project to create
170        name: PathBuf,
171
172        /// Project archetype (basic, gateway, consumer)
173        #[arg(short, long, value_enum, default_value_t = CliArchetype::Basic)]
174        archetype: CliArchetype,
175
176        /// Service name (e.g., "kraken" for gateway, "order-processor" for
177        /// consumer)
178        #[arg(long)]
179        service_name: Option<String>,
180
181        /// Base URL for gateway's external API
182        #[arg(long)]
183        api_base_url: Option<String>,
184
185        /// Consumer group ID (for consumer archetype)
186        #[arg(long)]
187        group_id: Option<String>,
188
189        /// Kafka broker addresses (for consumer archetype)
190        #[arg(long)]
191        brokers: Option<String>,
192
193        /// Enable all features
194        #[arg(long)]
195        all_features: bool,
196    },
197    /// Saga generation and management commands
198    Saga {
199        #[command(subcommand)]
200        command: SagaCommands,
201    },
202    /// Generate code from LLM prompts (coming soon)
203    Forge {
204        /// The prompt for code generation
205        prompt: String,
206    },
207}
208
209/// Run the AllFrame CLI with command-line arguments.
210///
211/// This is the main entry point for the CLI, designed to be called from
212/// both the `allframe-forge` binary and the `allframe` binary wrapper.
213///
214/// # Errors
215/// Returns an error if command parsing fails or if the executed command fails.
216pub 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
242/// Create a new AllFrame project
243///
244/// This function orchestrates the creation of a new AllFrame project with
245/// Clean Architecture structure.
246fn 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    // Build project configuration based on archetype
267    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            // Configure gateway-specific settings
273            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                    // Default to project name
279                    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            // Configure consumer-specific settings
294            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                    // Default to project name
300                    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            // Configure producer-specific settings
317            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                    // Default to project name
323                    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            // Configure BFF-specific settings
334            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                    // Default to project name
340                    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            // Configure scheduled-specific settings
357            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                    // Default to project name
363                    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            // Configure websocket-specific settings
375            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                    // Default to project name
381                    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            // Configure saga-specific settings
393            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                    // Default to project name
399                    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            // Configure legacy adapter settings
411            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                    // Default to project name
417                    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    // Create directory structure and generate files based on archetype
431    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
537/// Convert a string to title case (e.g., "kraken" -> "Kraken")
538fn 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
551/// Handle saga-related commands
552fn 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
581/// Create a new saga with specified steps
582fn 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    // Parse steps from comma-separated string
587    let step_list: Vec<&str> = steps.split(',').map(|s| s.trim()).collect();
588
589    // Create saga directory if it doesn't exist
590    let saga_path = base_path.join(name.to_lowercase());
591    std::fs::create_dir_all(&saga_path)?;
592
593    // Generate saga files
594    generate_saga_files(&saga_path, name, &step_list)?;
595
596    println!("Saga '{}' created successfully!", name);
597    Ok(())
598}
599
600/// Add a step to an existing saga
601fn 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
624    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
633/// Validate saga implementation
634fn 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    // Basic validation - check if files exist
643    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
662/// Generate saga files
663fn generate_saga_files(saga_path: &Path, name: &str, steps: &[&str]) -> anyhow::Result<()> {
664    // Generate mod.rs
665    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    // Generate saga implementation file
686    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    // Generate step files
691    for step in steps {
692        generate_step_file(saga_path, step, 30, true)?;
693    }
694
695    Ok(())
696}
697
698/// Generate saga implementation content
699fn 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
787/// Generate step file content
788fn 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
853/// Generate code from LLM prompts (not yet implemented)
854fn forge_code(_prompt: &str) -> anyhow::Result<()> {
855    anyhow::bail!("allframe forge is not yet implemented")
856}