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}