cargo_detect_package/
types.rs

1// Public API types for cargo-detect-package.
2//
3// These types are used by main.rs and exposed via the crate's public API.
4
5use std::fmt;
6use std::path::PathBuf;
7
8/// Action to take when a path is not within any package.
9#[derive(Clone, Debug, Eq, PartialEq)]
10#[non_exhaustive]
11pub enum OutsidePackageAction {
12    /// Use the entire workspace.
13    Workspace,
14    /// Ignore and do not run the subcommand, exit with success.
15    Ignore,
16    /// Error and do not run the subcommand, exit with failure.
17    Error,
18}
19
20// Mutations to match arms cause integration test timeouts due to cargo subprocess hangs.
21#[cfg_attr(test, mutants::skip)]
22impl std::str::FromStr for OutsidePackageAction {
23    type Err = String;
24
25    fn from_str(s: &str) -> Result<Self, Self::Err> {
26        match s.to_lowercase().as_str() {
27            "workspace" => Ok(Self::Workspace),
28            "ignore" => Ok(Self::Ignore),
29            "error" => Ok(Self::Error),
30            _ => Err(format!(
31                "Invalid outside-package action: '{s}'. Valid options are: workspace, ignore, error"
32            )),
33        }
34    }
35}
36
37/// Input parameters for the `run` function.
38///
39/// This is the parsed and validated input that the core logic operates on.
40#[doc(hidden)]
41#[derive(Debug)]
42#[allow(
43    clippy::exhaustive_structs,
44    reason = "This is a hidden struct for internal/test use only"
45)]
46pub struct RunInput {
47    /// Path to the file to detect package for.
48    pub path: PathBuf,
49    /// Pass the detected package as an environment variable instead of as a cargo argument.
50    pub via_env: Option<String>,
51    /// Action to take when path is not in any package.
52    pub outside_package: OutsidePackageAction,
53    /// The subcommand to execute.
54    pub subcommand: Vec<String>,
55}
56
57/// The outcome of a successful run.
58#[doc(hidden)]
59#[derive(Clone, Debug, Eq, PartialEq)]
60#[allow(
61    clippy::exhaustive_enums,
62    reason = "This is a hidden enum for internal/test use only"
63)]
64pub enum RunOutcome {
65    /// A package was detected and the subcommand executed successfully.
66    PackageDetected {
67        /// The name of the detected package.
68        package_name: String,
69        /// Whether the subcommand succeeded.
70        subcommand_succeeded: bool,
71    },
72    /// The path was not in any package, workspace scope was used.
73    WorkspaceScope {
74        /// Whether the subcommand succeeded.
75        subcommand_succeeded: bool,
76    },
77    /// The path was not in any package and was ignored (no subcommand executed).
78    Ignored,
79}
80
81/// Errors that can occur during a run.
82#[doc(hidden)]
83#[derive(Debug)]
84#[allow(
85    clippy::exhaustive_enums,
86    reason = "This is a hidden enum for internal/test use only"
87)]
88pub enum RunError {
89    /// Failed to validate workspace context.
90    WorkspaceValidation(String),
91    /// Failed to detect package.
92    PackageDetection(String),
93    /// Path is not in any package and --outside-package=error was specified.
94    OutsidePackage,
95    /// Failed to execute the subcommand.
96    CommandExecution(std::io::Error),
97}
98
99impl fmt::Display for RunError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::WorkspaceValidation(msg) => write!(f, "{msg}"),
103            Self::PackageDetection(msg) => write!(f, "Error detecting package: {msg}"),
104            Self::OutsidePackage => write!(f, "Path is not in any package"),
105            Self::CommandExecution(e) => write!(f, "Error executing command: {e}"),
106        }
107    }
108}
109
110impl std::error::Error for RunError {}
111
112#[cfg(test)]
113#[cfg_attr(coverage_nightly, coverage(off))]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn outside_package_action_parsing() {
119        assert_eq!(
120            "workspace".parse::<OutsidePackageAction>().unwrap(),
121            OutsidePackageAction::Workspace
122        );
123        assert_eq!(
124            "Workspace".parse::<OutsidePackageAction>().unwrap(),
125            OutsidePackageAction::Workspace
126        );
127        assert_eq!(
128            "WORKSPACE".parse::<OutsidePackageAction>().unwrap(),
129            OutsidePackageAction::Workspace
130        );
131
132        assert_eq!(
133            "ignore".parse::<OutsidePackageAction>().unwrap(),
134            OutsidePackageAction::Ignore
135        );
136        assert_eq!(
137            "Ignore".parse::<OutsidePackageAction>().unwrap(),
138            OutsidePackageAction::Ignore
139        );
140
141        assert_eq!(
142            "error".parse::<OutsidePackageAction>().unwrap(),
143            OutsidePackageAction::Error
144        );
145        assert_eq!(
146            "Error".parse::<OutsidePackageAction>().unwrap(),
147            OutsidePackageAction::Error
148        );
149
150        let result = "invalid".parse::<OutsidePackageAction>();
151        assert!(result.is_err());
152        assert!(
153            result
154                .unwrap_err()
155                .contains("Invalid outside-package action")
156        );
157    }
158
159    #[test]
160    fn run_error_display_workspace_validation() {
161        let error = RunError::WorkspaceValidation("some validation error".to_string());
162        let display = format!("{error}");
163        assert_eq!(display, "some validation error");
164    }
165
166    #[test]
167    fn run_error_display_package_detection() {
168        let error = RunError::PackageDetection("could not find package".to_string());
169        let display = format!("{error}");
170        assert_eq!(display, "Error detecting package: could not find package");
171    }
172
173    #[test]
174    fn run_error_display_outside_package() {
175        let error = RunError::OutsidePackage;
176        let display = format!("{error}");
177        assert_eq!(display, "Path is not in any package");
178    }
179
180    #[test]
181    fn run_error_display_command_execution() {
182        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "command not found");
183        let error = RunError::CommandExecution(io_error);
184        let display = format!("{error}");
185        assert!(display.starts_with("Error executing command:"));
186        assert!(display.contains("command not found"));
187    }
188}