Skip to main content

rusty_stack/adapter/
rust_adapter.rs

1//! Rust-native adapter — executes component operations without spawning shell subprocesses.
2//!
3//! This module provides:
4//! - [`RustAdapter`] — a generic adapter that wraps a Rust-native function
5//! - [`RustAdapterFn`] — type alias for Rust-native executor functions
6//!
7//! # Design
8//!
9//! Each `RustAdapter` wraps a function pointer or closure that performs the
10//! actual component operation in Rust. The adapter produces output that matches
11//! the format of the legacy shell scripts for behavior parity.
12//!
13//! # Validation Assertions
14//!
15//! - **VAL-MIGR-009**: Rust-registered adapter routes to Rust impl, no shell subprocess
16//! - **VAL-MIGR-012**: Output format matches shell for migrated components
17//! - **VAL-MIGR-013**: Exit codes match shell for success/failure/partial cases
18
19use super::{Adapter, AdapterError, AdapterOutput};
20use crate::core::types::ExecutorKind;
21use std::time::Instant;
22
23// ===========================================================================
24// Rust Adapter Function Type
25// ===========================================================================
26
27/// A Rust-native executor function.
28///
29/// Takes the component ID and target version, returns:
30/// - `Ok((stdout, stderr))` on success (exit code 0)
31/// - `Err((exit_code, stdout, stderr))` on failure
32pub type RustAdapterFn =
33    Box<dyn Fn(&str, &str) -> Result<(String, String), (i32, String, String)> + Send + Sync>;
34
35// ===========================================================================
36// RustAdapter
37// ===========================================================================
38
39/// Adapter that routes component operations to native Rust implementations.
40///
41/// This adapter does NOT spawn any shell subprocess. All work is done
42/// within the Rust process, producing output that matches the format
43/// of legacy shell scripts.
44///
45/// # Example
46///
47/// ```rust,ignore
48/// use rusty_stack::adapter::rust_adapter::RustAdapter;
49/// use rusty_stack::adapter::AdapterRegistry;
50///
51/// let adapter = RustAdapter::new(Box::new(|id, version| {
52///     Ok((format!("{id}: {version} installed"), String::new()))
53/// }));
54///
55/// let mut registry = AdapterRegistry::new();
56/// registry.register_rust("pytorch", Box::new(adapter));
57/// ```
58pub struct RustAdapter {
59    /// The native Rust executor function.
60    executor: RustAdapterFn,
61    /// Component ID this adapter handles (for error messages).
62    #[allow(dead_code)]
63    component_id: String,
64}
65
66impl RustAdapter {
67    /// Create a new Rust adapter with the given executor function.
68    pub fn new(executor: RustAdapterFn) -> Self {
69        Self {
70            executor,
71            component_id: String::new(),
72        }
73    }
74
75    /// Create a new Rust adapter with a component ID label.
76    pub fn with_id(component_id: impl Into<String>, executor: RustAdapterFn) -> Self {
77        Self {
78            executor,
79            component_id: component_id.into(),
80        }
81    }
82
83    /// Create a simple Rust adapter that always succeeds with a fixed message.
84    pub fn simple_success(message_prefix: &str) -> Self {
85        let prefix = message_prefix.to_string();
86        Self::new(Box::new(move |id, version| {
87            Ok((format!("{prefix}: {id} {version} installed"), String::new()))
88        }))
89    }
90
91    /// Create a Rust adapter that always fails with a fixed exit code.
92    pub fn always_fails(exit_code: i32, message: &str) -> Self {
93        let msg = message.to_string();
94        Self::new(Box::new(move |_id, _version| {
95            Err((exit_code, String::new(), msg.clone()))
96        }))
97    }
98}
99
100impl Adapter for RustAdapter {
101    fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError> {
102        let start = Instant::now();
103
104        match (self.executor)(component_id, version) {
105            Ok((stdout, stderr)) => Ok(AdapterOutput {
106                component_id: component_id.to_string(),
107                executor_kind: ExecutorKind::Rust,
108                stdout,
109                stderr,
110                exit_code: 0,
111                duration_ms: start.elapsed().as_millis() as u64,
112            }),
113            Err((exit_code, stdout, stderr)) => Ok(AdapterOutput {
114                component_id: component_id.to_string(),
115                executor_kind: ExecutorKind::Rust,
116                stdout,
117                stderr: stderr.clone(),
118                exit_code,
119                duration_ms: start.elapsed().as_millis() as u64,
120            }),
121        }
122    }
123
124    fn executor_kind(&self) -> ExecutorKind {
125        ExecutorKind::Rust
126    }
127}
128
129// ===========================================================================
130// Output Format Helpers
131// ===========================================================================
132
133/// Shell-compatible output formatter.
134///
135/// Produces output strings that match the format of legacy shell scripts,
136/// ensuring behavior parity during migration.
137pub struct ShellOutputFormat;
138
139impl ShellOutputFormat {
140    /// Format a success message matching shell script convention.
141    ///
142    /// Shell scripts typically output: `[component_name] [version] installed successfully`
143    pub fn success(component_id: &str, version: &str) -> String {
144        let display = crate::platform::registry::display_name(component_id);
145        format!("{display} {version} installed successfully")
146    }
147
148    /// Format a failure message matching shell script convention.
149    ///
150    /// Shell scripts typically output: `Error: [component_name] installation failed: [reason]`
151    pub fn failure(component_id: &str, reason: &str) -> String {
152        let display = crate::platform::registry::display_name(component_id);
153        format!("Error: {display} installation failed: {reason}")
154    }
155
156    /// Format a partial success message.
157    ///
158    /// Shell scripts typically output: `Warning: [component_name] partially installed: [details]`
159    pub fn partial(component_id: &str, details: &str) -> String {
160        let display = crate::platform::registry::display_name(component_id);
161        format!("Warning: {display} partially installed: {details}")
162    }
163
164    /// Format a status line matching shell script convention.
165    ///
166    /// Shell scripts typically output: `[component_name] [status]`
167    pub fn status(component_id: &str, status: &str) -> String {
168        let display = crate::platform::registry::display_name(component_id);
169        format!("{display} {status}")
170    }
171}
172
173// ===========================================================================
174// Tests
175// ===========================================================================
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    // -----------------------------------------------------------------------
182    // RustAdapter basic functionality
183    // -----------------------------------------------------------------------
184
185    #[test]
186    fn test_rust_adapter_success() {
187        let adapter = RustAdapter::new(Box::new(|id, version| {
188            Ok((format!("{id}:{version}:ok"), String::new()))
189        }));
190
191        let result = adapter.execute("pytorch", "2.5.0").unwrap();
192        assert_eq!(result.exit_code, 0);
193        assert_eq!(result.stdout, "pytorch:2.5.0:ok");
194        assert!(result.stderr.is_empty());
195        assert_eq!(result.executor_kind, ExecutorKind::Rust);
196    }
197
198    #[test]
199    fn test_rust_adapter_failure() {
200        let adapter = RustAdapter::new(Box::new(|_id, _version| {
201            Err((1, String::new(), "installation error".to_string()))
202        }));
203
204        let result = adapter.execute("pytorch", "2.5.0").unwrap();
205        assert_eq!(result.exit_code, 1);
206        assert_eq!(result.stderr, "installation error");
207        assert!(!result.is_success());
208    }
209
210    #[test]
211    fn test_rust_adapter_partial_failure() {
212        let adapter = RustAdapter::new(Box::new(|_id, _version| {
213            Err((2, "partial output".to_string(), "some warnings".to_string()))
214        }));
215
216        let result = adapter.execute("triton", "3.1.0").unwrap();
217        assert_eq!(result.exit_code, 2);
218        assert_eq!(result.stdout, "partial output");
219        assert_eq!(result.stderr, "some warnings");
220    }
221
222    #[test]
223    fn test_rust_adapter_no_shell_subprocess() {
224        // Verify that running a RustAdapter never touches the filesystem
225        // or spawns processes — it's all in-process Rust logic.
226        let adapter = RustAdapter::new(Box::new(|id, version| {
227            // Pure Rust computation, no subprocess
228            let result = format!("computed: {id}@{version}");
229            Ok((result, String::new()))
230        }));
231
232        let result = adapter.execute("test", "1.0.0").unwrap();
233        assert_eq!(result.stdout, "computed: test@1.0.0");
234        assert_eq!(result.executor_kind, ExecutorKind::Rust);
235    }
236
237    // -----------------------------------------------------------------------
238    // RustAdapter constructors
239    // -----------------------------------------------------------------------
240
241    #[test]
242    fn test_rust_adapter_simple_success() {
243        let adapter = RustAdapter::simple_success("mock");
244        let result = adapter.execute("pytorch", "2.5.0").unwrap();
245        assert_eq!(result.exit_code, 0);
246        assert!(result.stdout.contains("pytorch"));
247        assert!(result.stdout.contains("2.5.0"));
248    }
249
250    #[test]
251    fn test_rust_adapter_always_fails() {
252        let adapter = RustAdapter::always_fails(42, "always fails");
253        let result = adapter.execute("test", "1.0.0").unwrap();
254        assert_eq!(result.exit_code, 42);
255        assert_eq!(result.stderr, "always fails");
256    }
257
258    #[test]
259    fn test_rust_adapter_with_id() {
260        let adapter = RustAdapter::with_id(
261            "pytorch",
262            Box::new(|id, v| Ok((format!("{id}:{v}"), String::new()))),
263        );
264        let result = adapter.execute("pytorch", "2.5.0").unwrap();
265        assert!(result.stdout.contains("pytorch"));
266    }
267
268    // -----------------------------------------------------------------------
269    // ShellOutputFormat parity
270    // -----------------------------------------------------------------------
271
272    #[test]
273    fn test_shell_output_format_success() {
274        let output = ShellOutputFormat::success("pytorch", "2.5.0");
275        assert!(output.contains("PyTorch"));
276        assert!(output.contains("2.5.0"));
277        assert!(output.contains("installed successfully"));
278    }
279
280    #[test]
281    fn test_shell_output_format_failure() {
282        let output = ShellOutputFormat::failure("pytorch", "build error");
283        assert!(output.contains("PyTorch"));
284        assert!(output.contains("Error:"));
285        assert!(output.contains("build error"));
286    }
287
288    #[test]
289    fn test_shell_output_format_partial() {
290        let output = ShellOutputFormat::partial("triton", "missing optional deps");
291        assert!(output.contains("Triton"));
292        assert!(output.contains("Warning:"));
293        assert!(output.contains("missing optional deps"));
294    }
295
296    #[test]
297    fn test_shell_output_format_status() {
298        let output = ShellOutputFormat::status("rocm", "detected");
299        assert!(output.contains("ROCm"));
300        assert!(output.contains("detected"));
301    }
302
303    // -----------------------------------------------------------------------
304    // Duration tracking
305    // -----------------------------------------------------------------------
306
307    #[test]
308    fn test_rust_adapter_tracks_duration() {
309        let adapter = RustAdapter::new(Box::new(|_id, _version| {
310            // Simulate some work
311            std::thread::sleep(std::time::Duration::from_millis(1));
312            Ok(("done".to_string(), String::new()))
313        }));
314
315        let result = adapter.execute("test", "1.0.0").unwrap();
316        assert!(result.duration_ms > 0, "Duration should be non-zero");
317    }
318
319    // -----------------------------------------------------------------------
320    // Executor kind
321    // -----------------------------------------------------------------------
322
323    #[test]
324    fn test_rust_adapter_executor_kind() {
325        let adapter = RustAdapter::simple_success("test");
326        assert_eq!(adapter.executor_kind(), ExecutorKind::Rust);
327    }
328}