Skip to main content

command_stream/
utils.rs

1//! Utility functions and types for command-stream
2//!
3//! This module provides helper functions for command results, virtual command
4//! utilities, and re-exports from specialized utility modules.
5//!
6//! ## Module Organization
7//!
8//! The utilities are organized into focused modules following the same
9//! modular pattern as the JavaScript implementation:
10//!
11//! - `trace` - Logging and tracing utilities
12//! - `ansi` - ANSI escape code handling
13//! - `quote` - Shell quoting utilities
14//! - `utils` (this module) - Command results and virtual command helpers
15
16use std::env;
17use std::path::{Path, PathBuf};
18
19// Re-export from specialized modules for backwards compatibility
20pub use crate::ansi::{AnsiConfig, AnsiUtils};
21pub use crate::quote::quote;
22pub use crate::trace::{is_trace_enabled, trace, trace_lazy};
23
24/// Result type for virtual command operations
25#[derive(Debug, Clone)]
26pub struct CommandResult {
27    pub stdout: String,
28    pub stderr: String,
29    pub code: i32,
30}
31
32impl CommandResult {
33    /// Create a success result with stdout output
34    pub fn success(stdout: impl Into<String>) -> Self {
35        CommandResult {
36            stdout: stdout.into(),
37            stderr: String::new(),
38            code: 0,
39        }
40    }
41
42    /// Create an empty success result
43    pub fn success_empty() -> Self {
44        CommandResult {
45            stdout: String::new(),
46            stderr: String::new(),
47            code: 0,
48        }
49    }
50
51    /// Create an error result with stderr output
52    pub fn error(stderr: impl Into<String>) -> Self {
53        CommandResult {
54            stdout: String::new(),
55            stderr: stderr.into(),
56            code: 1,
57        }
58    }
59
60    /// Create an error result with custom exit code
61    pub fn error_with_code(stderr: impl Into<String>, code: i32) -> Self {
62        CommandResult {
63            stdout: String::new(),
64            stderr: stderr.into(),
65            code,
66        }
67    }
68
69    /// Check if the command was successful
70    pub fn is_success(&self) -> bool {
71        self.code == 0
72    }
73
74    /// Exit code of the command.
75    ///
76    /// This is an alias for the [`code`](Self::code) field, mirroring the
77    /// `exitCode` alias exposed by the JavaScript implementation (issue #36).
78    pub fn exit_code(&self) -> i32 {
79        self.code
80    }
81}
82
83/// Utility functions for virtual commands
84pub struct VirtualUtils;
85
86impl VirtualUtils {
87    /// Create standardized error response for missing operands
88    pub fn missing_operand_error(command_name: &str) -> CommandResult {
89        CommandResult::error(format!("{}: missing operand", command_name))
90    }
91
92    /// Create standardized error response for missing operands with custom message
93    pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
94        CommandResult::error(format!("{}: {}", command_name, message))
95    }
96
97    /// Create standardized error response for invalid arguments
98    pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
99        CommandResult::error(format!("{}: {}", command_name, message))
100    }
101
102    /// Create standardized success response
103    pub fn success(stdout: impl Into<String>) -> CommandResult {
104        CommandResult::success(stdout)
105    }
106
107    /// Create standardized error response
108    pub fn error(stderr: impl Into<String>) -> CommandResult {
109        CommandResult::error(stderr)
110    }
111
112    /// Validate that command has required number of arguments
113    pub fn validate_args(
114        args: &[String],
115        min_count: usize,
116        command_name: &str,
117    ) -> Option<CommandResult> {
118        if args.len() < min_count {
119            if min_count == 1 {
120                return Some(Self::missing_operand_error(command_name));
121            } else {
122                return Some(Self::invalid_argument_error(
123                    command_name,
124                    &format!("requires at least {} arguments", min_count),
125                ));
126            }
127        }
128        None // No error
129    }
130
131    /// Resolve file path with optional cwd parameter
132    pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf {
133        let path = Path::new(file_path);
134        if path.is_absolute() {
135            path.to_path_buf()
136        } else {
137            let base_path = cwd
138                .map(|p| p.to_path_buf())
139                .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
140            base_path.join(path)
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_command_result_success() {
151        let result = CommandResult::success("hello");
152        assert!(result.is_success());
153        assert_eq!(result.stdout, "hello");
154        assert_eq!(result.stderr, "");
155        assert_eq!(result.code, 0);
156    }
157
158    #[test]
159    fn test_command_result_error() {
160        let result = CommandResult::error("something went wrong");
161        assert!(!result.is_success());
162        assert_eq!(result.stdout, "");
163        assert_eq!(result.stderr, "something went wrong");
164        assert_eq!(result.code, 1);
165    }
166
167    #[test]
168    fn test_command_result_error_with_code() {
169        let result = CommandResult::error_with_code("permission denied", 126);
170        assert!(!result.is_success());
171        assert_eq!(result.code, 126);
172    }
173
174    #[test]
175    fn test_resolve_path_absolute() {
176        let absolute_path = if cfg!(windows) {
177            PathBuf::from(r"C:\absolute\path")
178        } else {
179            PathBuf::from("/absolute/path")
180        };
181        let path = VirtualUtils::resolve_path(absolute_path.to_str().unwrap(), None);
182        assert_eq!(path, absolute_path);
183    }
184
185    #[test]
186    fn test_resolve_path_relative() {
187        let cwd = PathBuf::from("/home/user");
188        let path = VirtualUtils::resolve_path("relative/path", Some(&cwd));
189        assert_eq!(path, PathBuf::from("/home/user/relative/path"));
190    }
191
192    #[test]
193    fn test_validate_args_success() {
194        let args = vec!["arg1".to_string()];
195        assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
196    }
197
198    #[test]
199    fn test_validate_args_missing() {
200        let args = vec!["arg1".to_string()];
201        let result = VirtualUtils::validate_args(&args, 2, "cmd");
202        assert!(result.is_some());
203    }
204
205    #[test]
206    fn test_missing_operand_error() {
207        let result = VirtualUtils::missing_operand_error("cat");
208        assert!(!result.is_success());
209        assert!(result.stderr.contains("missing operand"));
210    }
211
212    #[test]
213    fn test_invalid_argument_error() {
214        let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
215        assert!(!result.is_success());
216        assert!(result.stderr.contains("invalid option"));
217    }
218
219    // Re-exported module tests are in their respective modules
220    // These tests verify the re-exports work correctly
221
222    #[test]
223    fn test_reexported_quote() {
224        assert_eq!(quote("hello"), "hello");
225        assert_eq!(quote("hello world"), "'hello world'");
226    }
227
228    #[test]
229    fn test_reexported_ansi_utils() {
230        let text = "\x1b[31mRed text\x1b[0m";
231        assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
232    }
233
234    #[test]
235    fn test_reexported_ansi_config() {
236        let config = AnsiConfig::default();
237        assert!(config.preserve_ansi);
238        assert!(config.preserve_control_chars);
239    }
240}