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    },
190    /// Saga generation and management commands
191    Saga {
192        #[command(subcommand)]
193        command: SagaCommands,
194    },
195}
196
197/// Run the AllFrame CLI with command-line arguments.
198///
199/// This is the main entry point for the CLI, designed to be called from
200/// both the `allframe-forge` binary and the `allframe` binary wrapper.
201///
202/// # Errors
203/// Returns an error if command parsing fails or if the executed command fails.
204pub 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
225/// Create a new AllFrame project
226///
227/// This function orchestrates the creation of a new AllFrame project with
228/// Clean Architecture structure.
229fn 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    // Build project configuration based on archetype
250    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            // Configure gateway-specific settings
256            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                    // Default to project name
262                    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            // Configure consumer-specific settings
277            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                    // Default to project name
283                    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            // Configure producer-specific settings
300            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                    // Default to project name
306                    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            // Configure BFF-specific settings
317            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                    // Default to project name
323                    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            // Configure scheduled-specific settings
340            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                    // Default to project name
346                    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            // Configure websocket-specific settings
358            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                    // Default to project name
364                    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            // Configure saga-specific settings
376            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                    // Default to project name
382                    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            // Configure legacy adapter settings
394            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                    // Default to project name
400                    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    // Create directory structure and generate files based on archetype
414    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
520/// Convert a string to title case (e.g., "kraken" -> "Kraken")
521fn 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
534/// Handle saga-related commands
535fn 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
564/// Create a new saga with specified steps
565fn 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    // Parse steps from comma-separated string
570    let step_list: Vec<&str> = steps.split(',').map(|s| s.trim()).collect();
571
572    // Create saga directory if it doesn't exist
573    let saga_path = base_path.join(name.to_lowercase());
574    std::fs::create_dir_all(&saga_path)?;
575
576    // Generate saga files
577    generate_saga_files(&saga_path, name, &step_list)?;
578
579    println!("Saga '{}' created successfully!", name);
580    Ok(())
581}
582
583/// Add a step to an existing saga
584fn 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
607    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
616/// Validate saga implementation
617fn 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    // Basic validation - check if files exist
626    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
645/// Generate saga files
646fn generate_saga_files(saga_path: &Path, name: &str, steps: &[&str]) -> anyhow::Result<()> {
647    // Generate mod.rs
648    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    // Generate saga implementation file
669    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    // Generate step files
674    for step in steps {
675        generate_step_file(saga_path, step, 30, true)?;
676    }
677
678    Ok(())
679}
680
681/// Generate saga implementation content
682fn 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
770/// Generate step file content
771fn 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