Skip to main content

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