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.4.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,ignore
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.to_string()))
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(
39            "Cannot create runtime from within a runtime. Use Handle::current() instead."
40                .to_string(),
41        );
42    }
43    Runtime::new().map_err(|e| format!("Failed to create async runtime: {}", e))
44}
45
46/// Execute an async function in a sync context
47///
48/// Detects if we're already in a tokio runtime and uses Handle::current() if so,
49/// otherwise creates a new runtime. This prevents nested runtime panics.
50///
51/// # Examples
52///
53/// ```rust,ignore
54/// use cli::runtime_helper::execute_async;
55///
56/// fn sync_function() -> Result<String, String> {
57///     execute_async(async {
58///         let data = fetch_data().await?;
59///         Ok(data)
60///     })
61/// }
62/// ```
63///
64/// # Errors
65///
66/// Returns error if:
67/// - Runtime creation fails
68/// - The async future returns an error
69pub fn execute_async<F, T>(future: F) -> Result<T, String>
70where
71    F: std::future::Future<Output = Result<T, String>> + Send + 'static,
72    T: Send + 'static,
73{
74    // Check if we're already in a tokio runtime
75    match tokio::runtime::Handle::try_current() {
76        Ok(_handle) => {
77            // We're in a runtime, spawn a blocking task to run the future in a new thread
78            std::thread::scope(|s| {
79                s.spawn(|| {
80                    // Create a new runtime in this thread
81                    let rt = Runtime::new()
82                        .map_err(|e| format!("Failed to create async runtime: {}", e))?;
83                    rt.block_on(future)
84                })
85                .join()
86                .unwrap_or_else(|e| Err(format!("Thread panicked: {:?}", e)))
87            })
88        }
89        Err(_) => {
90            // No runtime, create one
91            let rt =
92                Runtime::new().map_err(|e| format!("Failed to create async runtime: {}", e))?;
93            rt.block_on(future)
94        }
95    }
96}
97
98/// Execute an async function and convert errors to clap_noun_verb::NounVerbError
99///
100/// Detects if we're already in a tokio runtime and uses Handle::current() if so,
101/// otherwise creates a new runtime. Automatically converts anyhow errors to
102/// NounVerbError for use in verb functions.
103///
104/// # Examples
105///
106/// ```rust,ignore
107/// use cli::runtime_helper::execute_async_verb;
108/// use clap_noun_verb::Result;
109///
110/// #[verb("doctor", "utils")]
111/// fn utils_doctor() -> Result<DoctorOutput> {
112///     execute_async_verb(async {
113///         run_diagnostics().await
114///     })
115/// }
116/// ```
117pub fn execute_async_verb<F, T>(future: F) -> clap_noun_verb::Result<T>
118where
119    F: std::future::Future<Output = anyhow::Result<T>> + Send + 'static,
120    T: Send + 'static,
121{
122    // Check if we're already in a tokio runtime
123    match tokio::runtime::Handle::try_current() {
124        Ok(_handle) => {
125            // We're in a runtime, spawn a blocking task to run the future
126            // This prevents "Cannot start a runtime from within a runtime" error
127            std::thread::scope(|s| {
128                s.spawn(|| {
129                    // Create a new runtime in this thread
130                    let rt = Runtime::new().map_err(|e| {
131                        clap_noun_verb::NounVerbError::execution_error(format!(
132                            "Failed to create async runtime: {}",
133                            e
134                        ))
135                    })?;
136                    rt.block_on(future)
137                        .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
138                })
139                .join()
140                .unwrap_or_else(|e| {
141                    Err(clap_noun_verb::NounVerbError::execution_error(format!(
142                        "Thread panicked: {:?}",
143                        e
144                    )))
145                })
146            })
147        }
148        Err(_) => {
149            // No runtime, create one
150            let rt = Runtime::new().map_err(|e| {
151                clap_noun_verb::NounVerbError::execution_error(format!(
152                    "Failed to create async runtime: {}",
153                    e
154                ))
155            })?;
156            rt.block_on(future)
157                .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_create_runtime() {
168        let result = create_runtime();
169        assert!(result.is_ok(), "Runtime creation should succeed");
170    }
171
172    #[test]
173    fn test_execute_async_success() {
174        let result = execute_async(async { Ok::<i32, String>(42) });
175        assert_eq!(result, Ok(42));
176    }
177
178    #[test]
179    fn test_execute_async_error() {
180        let result = execute_async(async { Err::<i32, String>("test error".to_string()) });
181        assert!(result.is_err());
182        assert_eq!(result.unwrap_err(), "test error");
183    }
184
185    #[test]
186    fn test_execute_async_verb_success() {
187        let result = execute_async_verb(async { Ok::<i32, anyhow::Error>(42) });
188        assert!(result.is_ok());
189    }
190
191    #[test]
192    fn test_execute_async_verb_error() {
193        let result =
194            execute_async_verb(async { Err::<i32, anyhow::Error>(anyhow::anyhow!("test error")) });
195        assert!(result.is_err());
196    }
197}