aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/exit_code.rs
blob: bc01cfa467ad0dda2b3865d344dfc782c0502488 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/// Exit code: success.
pub const OK: i32 = 0;

/// Exit code: general / unclassified error.
pub const GENERAL_ERROR: i32 = 1;

/// Exit code: dependency resolution failed.
pub const DEPENDENCY_RESOLUTION_FAILED: i32 = 2;

/// Exit code: partial update requested but no lock file exists.
pub const NO_LOCK_FILE_FOR_PARTIAL_UPDATE: i32 = 3;

/// Exit code: lock file is invalid or corrupt.
pub const LOCK_FILE_INVALID: i32 = 4;

/// Exit code: audit found a security advisory.
pub const AUDIT_FAILED: i32 = 5;

/// Exit code: HTTP / network transport error.
pub const TRANSPORT_ERROR: i32 = 100;

// ---------------------------------------------------------------------------
// MozartError — carries a specific exit code through anyhow's error chain
// ---------------------------------------------------------------------------

/// An error type that carries a specific exit code for Mozart to use on exit.
///
/// Use [`bail`] or [`bail_silent`] to construct one wrapped in `anyhow::Error`.
#[derive(Debug)]
pub struct MozartError {
    pub message: String,
    pub exit_code: i32,
}

impl std::fmt::Display for MozartError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for MozartError {}

/// Return an `anyhow::Error` that carries `exit_code` and prints `message`.
pub fn bail(exit_code: i32, message: impl Into<String>) -> anyhow::Error {
    MozartError {
        message: message.into(),
        exit_code,
    }
    .into()
}

/// Return an `anyhow::Error` that carries `exit_code` but suppresses the
/// message (caller has already printed it).
pub fn bail_silent(exit_code: i32) -> anyhow::Error {
    MozartError {
        message: String::new(),
        exit_code,
    }
    .into()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_constants_have_expected_values() {
        assert_eq!(OK, 0);
        assert_eq!(GENERAL_ERROR, 1);
        assert_eq!(DEPENDENCY_RESOLUTION_FAILED, 2);
        assert_eq!(NO_LOCK_FILE_FOR_PARTIAL_UPDATE, 3);
        assert_eq!(LOCK_FILE_INVALID, 4);
        assert_eq!(AUDIT_FAILED, 5);
        assert_eq!(TRANSPORT_ERROR, 100);
    }

    #[test]
    fn test_mozart_error_display() {
        let err = MozartError {
            message: "something went wrong".to_string(),
            exit_code: GENERAL_ERROR,
        };
        assert_eq!(format!("{err}"), "something went wrong");
    }

    #[test]
    fn test_bail_can_be_downcast() {
        let err = bail(DEPENDENCY_RESOLUTION_FAILED, "cannot resolve");
        let me = err.downcast_ref::<MozartError>().expect("should downcast");
        assert_eq!(me.exit_code, DEPENDENCY_RESOLUTION_FAILED);
        assert_eq!(me.message, "cannot resolve");
    }

    #[test]
    fn test_bail_silent_has_empty_message() {
        let err = bail_silent(GENERAL_ERROR);
        let me = err.downcast_ref::<MozartError>().expect("should downcast");
        assert_eq!(me.exit_code, GENERAL_ERROR);
        assert!(me.message.is_empty());
    }

    #[test]
    fn test_mozart_error_is_std_error() {
        let err: Box<dyn std::error::Error> = Box::new(MozartError {
            message: "test".to_string(),
            exit_code: 1,
        });
        assert_eq!(err.to_string(), "test");
    }
}