ggen_cli_lib/
runtime_helper.rs

1//! Runtime helper for sync CLI wrappers
2//!
3//! This module provides utilities for executing async operations in sync CLI contexts.
4//! Required because clap-noun-verb v3.0.0 uses sync verb functions, but business logic
5//! may require async operations (file I/O, network requests, etc.).
6//!
7//! # Examples
8//!
9//! ```rust
10//! use cli::runtime_helper::execute_async;
11//! use clap_noun_verb::Result;
12//!
13//! fn my_sync_command() -> Result<Output> {
14//!     execute_async(async {
15//!         // Async business logic here
16//!         let result = async_operation().await?;
17//!         Ok(result)
18//!     })
19//!     .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e))
20//! }
21//! ```
22
23use tokio::runtime::Runtime;
24
25/// Create a new tokio runtime for async operations in sync context
26///
27/// IMPORTANT: This function detects if we're already inside a tokio runtime
28/// (e.g., when using #[tokio::main]) and returns an error in that case.
29/// Use execute_async() or execute_async_verb() instead, which handle this properly.
30///
31/// # Errors
32///
33/// Returns error if runtime creation fails (rare, usually indicates system resource issues)
34/// or if called from within an existing runtime.
35pub fn create_runtime() -> Result<Runtime, String> {
36    // Check if we're already in a tokio runtime
37    if tokio::runtime::Handle::try_current().is_ok() {
38        return Err("Cannot create runtime from within a runtime. Use Handle::current() instead.".to_string());
39    }
40    Runtime::new().map_err(|e| format!("Failed to create async runtime: {}", e))
41}
42
43/// Execute an async function in a sync context
44///
45/// Detects if we're already in a tokio runtime and uses Handle::current() if so,
46/// otherwise creates a new runtime. This prevents nested runtime panics.
47///
48/// # Examples
49///
50/// ```rust
51/// use cli::runtime_helper::execute_async;
52///
53/// fn sync_function() -> Result<String, String> {
54///     execute_async(async {
55///         let data = fetch_data().await?;
56///         Ok(data)
57///     })
58/// }
59/// ```
60///
61/// # Errors
62///
63/// Returns error if:
64/// - Runtime creation fails
65/// - The async future returns an error
66pub fn execute_async<F, T>(future: F) -> Result<T, String>
67where
68    F: std::future::Future<Output = Result<T, String>> + Send + 'static,
69    T: Send + 'static,
70{
71    // Check if we're already in a tokio runtime
72    match tokio::runtime::Handle::try_current() {
73        Ok(_handle) => {
74            // We're in a runtime, spawn a blocking task to run the future in a new thread
75            std::thread::scope(|s| {
76                s.spawn(|| {
77                    // Create a new runtime in this thread
78                    let rt = Runtime::new()
79                        .map_err(|e| format!("Failed to create async runtime: {}", e))?;
80                    rt.block_on(future)
81                }).join().unwrap_or_else(|e| {
82                    Err(format!("Thread panicked: {:?}", e))
83                })
84            })
85        }
86        Err(_) => {
87            // No runtime, create one
88            let rt = Runtime::new().map_err(|e| format!("Failed to create async runtime: {}", e))?;
89            rt.block_on(future)
90        }
91    }
92}
93
94/// Execute an async function and convert errors to clap_noun_verb::NounVerbError
95///
96/// Detects if we're already in a tokio runtime and uses Handle::current() if so,
97/// otherwise creates a new runtime. Automatically converts anyhow errors to
98/// NounVerbError for use in verb functions.
99///
100/// # Examples
101///
102/// ```rust
103/// use cli::runtime_helper::execute_async_verb;
104/// use clap_noun_verb::Result;
105///
106/// #[verb("doctor", "utils")]
107/// fn utils_doctor() -> Result<DoctorOutput> {
108///     execute_async_verb(async {
109///         run_diagnostics().await
110///     })
111/// }
112/// ```
113pub fn execute_async_verb<F, T>(future: F) -> clap_noun_verb::Result<T>
114where
115    F: std::future::Future<Output = anyhow::Result<T>> + Send + 'static,
116    T: Send + 'static,
117{
118    // Check if we're already in a tokio runtime
119    match tokio::runtime::Handle::try_current() {
120        Ok(handle) => {
121            // We're in a runtime, spawn a blocking task to run the future
122            // This prevents "Cannot start a runtime from within a runtime" error
123            std::thread::scope(|s| {
124                s.spawn(|| {
125                    // Create a new runtime in this thread
126                    let rt = Runtime::new()
127                        .map_err(|e| clap_noun_verb::NounVerbError::execution_error(
128                            format!("Failed to create async runtime: {}", e)
129                        ))?;
130                    rt.block_on(future)
131                        .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
132                }).join().unwrap_or_else(|e| {
133                    Err(clap_noun_verb::NounVerbError::execution_error(
134                        format!("Thread panicked: {:?}", e)
135                    ))
136                })
137            })
138        }
139        Err(_) => {
140            // No runtime, create one
141            let rt = Runtime::new()
142                .map_err(|e| clap_noun_verb::NounVerbError::execution_error(
143                    format!("Failed to create async runtime: {}", e)
144                ))?;
145            rt.block_on(future)
146                .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_create_runtime() {
157        let result = create_runtime();
158        assert!(result.is_ok(), "Runtime creation should succeed");
159    }
160
161    #[test]
162    fn test_execute_async_success() {
163        let result = execute_async(async { Ok::<i32, String>(42) });
164        assert_eq!(result, Ok(42));
165    }
166
167    #[test]
168    fn test_execute_async_error() {
169        let result = execute_async(async { Err::<i32, String>("test error".to_string()) });
170        assert!(result.is_err());
171        assert_eq!(result.unwrap_err(), "test error");
172    }
173
174    #[test]
175    fn test_execute_async_verb_success() {
176        let result = execute_async_verb(async { Ok::<i32, String>(42) });
177        assert!(result.is_ok());
178    }
179
180    #[test]
181    fn test_execute_async_verb_error() {
182        let result = execute_async_verb(async { Err::<i32, String>("test error".to_string()) });
183        assert!(result.is_err());
184    }
185}