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}
62
63impl RustAdapter {
64    /// Create a new Rust adapter with the given executor function.
65    pub fn new(executor: RustAdapterFn) -> Self {
66        Self { executor }
67    }
68
69    /// Create a simple Rust adapter that always succeeds with a fixed message.
70    pub fn simple_success(message_prefix: &str) -> Self {
71        let prefix = message_prefix.to_string();
72        Self::new(Box::new(move |id, version| {
73            Ok((format!("{prefix}: {id} {version} installed"), String::new()))
74        }))
75    }
76
77    /// Create a Rust adapter that always fails with a fixed exit code.
78    pub fn always_fails(exit_code: i32, message: &str) -> Self {
79        let msg = message.to_string();
80        Self::new(Box::new(move |_id, _version| {
81            Err((exit_code, String::new(), msg.clone()))
82        }))
83    }
84}
85
86impl Adapter for RustAdapter {
87    fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError> {
88        let start = Instant::now();
89
90        match (self.executor)(component_id, version) {
91            Ok((stdout, stderr)) => Ok(AdapterOutput {
92                component_id: component_id.to_string(),
93                executor_kind: ExecutorKind::Rust,
94                stdout,
95                stderr,
96                exit_code: 0,
97                duration_ms: start.elapsed().as_millis() as u64,
98            }),
99            Err((exit_code, stdout, stderr)) => Ok(AdapterOutput {
100                component_id: component_id.to_string(),
101                executor_kind: ExecutorKind::Rust,
102                stdout,
103                stderr: stderr.clone(),
104                exit_code,
105                duration_ms: start.elapsed().as_millis() as u64,
106            }),
107        }
108    }
109
110    fn executor_kind(&self) -> ExecutorKind {
111        ExecutorKind::Rust
112    }
113}
114
115// ===========================================================================
116// Output Format Helpers
117// ===========================================================================
118
119/// Shell-compatible output formatter.
120///
121/// Produces output strings that match the format of legacy shell scripts,
122/// ensuring behavior parity during migration.
123pub struct ShellOutputFormat;
124
125impl ShellOutputFormat {
126    /// Format a success message matching shell script convention.
127    ///
128    /// Shell scripts typically output: `[component_name] [version] installed successfully`
129    pub fn success(component_id: &str, version: &str) -> String {
130        let display = crate::platform::registry::display_name(component_id);
131        format!("{display} {version} installed successfully")
132    }
133
134    /// Format a failure message matching shell script convention.
135    ///
136    /// Shell scripts typically output: `Error: [component_name] installation failed: [reason]`
137    pub fn failure(component_id: &str, reason: &str) -> String {
138        let display = crate::platform::registry::display_name(component_id);
139        format!("Error: {display} installation failed: {reason}")
140    }
141
142    /// Format a partial success message.
143    ///
144    /// Shell scripts typically output: `Warning: [component_name] partially installed: [details]`
145    pub fn partial(component_id: &str, details: &str) -> String {
146        let display = crate::platform::registry::display_name(component_id);
147        format!("Warning: {display} partially installed: {details}")
148    }
149
150    /// Format a status line matching shell script convention.
151    ///
152    /// Shell scripts typically output: `[component_name] [status]`
153    pub fn status(component_id: &str, status: &str) -> String {
154        let display = crate::platform::registry::display_name(component_id);
155        format!("{display} {status}")
156    }
157}
158
159// ===========================================================================
160// Tests
161// ===========================================================================
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    // -----------------------------------------------------------------------
168    // RustAdapter basic functionality
169    // -----------------------------------------------------------------------
170
171    #[test]
172    fn test_rust_adapter_success() {
173        let adapter = RustAdapter::new(Box::new(|id, version| {
174            Ok((format!("{id}:{version}:ok"), String::new()))
175        }));
176
177        let result = adapter.execute("pytorch", "2.5.0").unwrap();
178        assert_eq!(result.exit_code, 0);
179        assert_eq!(result.stdout, "pytorch:2.5.0:ok");
180        assert!(result.stderr.is_empty());
181        assert_eq!(result.executor_kind, ExecutorKind::Rust);
182    }
183
184    #[test]
185    fn test_rust_adapter_failure() {
186        let adapter = RustAdapter::new(Box::new(|_id, _version| {
187            Err((1, String::new(), "installation error".to_string()))
188        }));
189
190        let result = adapter.execute("pytorch", "2.5.0").unwrap();
191        assert_eq!(result.exit_code, 1);
192        assert_eq!(result.stderr, "installation error");
193        assert!(!result.is_success());
194    }
195
196    #[test]
197    fn test_rust_adapter_partial_failure() {
198        let adapter = RustAdapter::new(Box::new(|_id, _version| {
199            Err((2, "partial output".to_string(), "some warnings".to_string()))
200        }));
201
202        let result = adapter.execute("triton", "3.1.0").unwrap();
203        assert_eq!(result.exit_code, 2);
204        assert_eq!(result.stdout, "partial output");
205        assert_eq!(result.stderr, "some warnings");
206    }
207
208    #[test]
209    fn test_rust_adapter_no_shell_subprocess() {
210        // Verify that running a RustAdapter never touches the filesystem
211        // or spawns processes — it's all in-process Rust logic.
212        let adapter = RustAdapter::new(Box::new(|id, version| {
213            // Pure Rust computation, no subprocess
214            let result = format!("computed: {id}@{version}");
215            Ok((result, String::new()))
216        }));
217
218        let result = adapter.execute("test", "1.0.0").unwrap();
219        assert_eq!(result.stdout, "computed: test@1.0.0");
220        assert_eq!(result.executor_kind, ExecutorKind::Rust);
221    }
222
223    // -----------------------------------------------------------------------
224    // RustAdapter constructors
225    // -----------------------------------------------------------------------
226
227    #[test]
228    fn test_rust_adapter_simple_success() {
229        let adapter = RustAdapter::simple_success("mock");
230        let result = adapter.execute("pytorch", "2.5.0").unwrap();
231        assert_eq!(result.exit_code, 0);
232        assert!(result.stdout.contains("pytorch"));
233        assert!(result.stdout.contains("2.5.0"));
234    }
235
236    #[test]
237    fn test_rust_adapter_always_fails() {
238        let adapter = RustAdapter::always_fails(42, "always fails");
239        let result = adapter.execute("test", "1.0.0").unwrap();
240        assert_eq!(result.exit_code, 42);
241        assert_eq!(result.stderr, "always fails");
242    }
243
244    // -----------------------------------------------------------------------
245    // ShellOutputFormat parity
246    // -----------------------------------------------------------------------
247
248    #[test]
249    fn test_shell_output_format_success() {
250        let output = ShellOutputFormat::success("pytorch", "2.5.0");
251        assert!(output.contains("PyTorch"));
252        assert!(output.contains("2.5.0"));
253        assert!(output.contains("installed successfully"));
254    }
255
256    #[test]
257    fn test_shell_output_format_failure() {
258        let output = ShellOutputFormat::failure("pytorch", "build error");
259        assert!(output.contains("PyTorch"));
260        assert!(output.contains("Error:"));
261        assert!(output.contains("build error"));
262    }
263
264    #[test]
265    fn test_shell_output_format_partial() {
266        let output = ShellOutputFormat::partial("triton", "missing optional deps");
267        assert!(output.contains("Triton"));
268        assert!(output.contains("Warning:"));
269        assert!(output.contains("missing optional deps"));
270    }
271
272    #[test]
273    fn test_shell_output_format_status() {
274        let output = ShellOutputFormat::status("rocm", "detected");
275        assert!(output.contains("ROCm"));
276        assert!(output.contains("detected"));
277    }
278
279    // -----------------------------------------------------------------------
280    // Duration tracking
281    // -----------------------------------------------------------------------
282
283    #[test]
284    fn test_rust_adapter_tracks_duration() {
285        let adapter = RustAdapter::new(Box::new(|_id, _version| {
286            // Simulate some work
287            std::thread::sleep(std::time::Duration::from_millis(1));
288            Ok(("done".to_string(), String::new()))
289        }));
290
291        let result = adapter.execute("test", "1.0.0").unwrap();
292        assert!(result.duration_ms > 0, "Duration should be non-zero");
293    }
294
295    // -----------------------------------------------------------------------
296    // Executor kind
297    // -----------------------------------------------------------------------
298
299    #[test]
300    fn test_rust_adapter_executor_kind() {
301        let adapter = RustAdapter::simple_success("test");
302        assert_eq!(adapter.executor_kind(), ExecutorKind::Rust);
303    }
304}