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}