Skip to main content

siderite_cli/
error.rs

1//! Errors of the application command line.
2//!
3//! No variant carries a database URL, password or other secret: URLs are
4//! reduced to their scheme before they reach an error message.
5
6use siderite_core::ServerError;
7use siderite_migrations::MigrationError;
8use thiserror::Error;
9
10/// Failure of an [`AppCli`](crate::AppCli) command.
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum CliError {
14    /// Bad command line (unknown command or flag, missing value).
15    #[error("{0}")]
16    Usage(String),
17    /// No database URL is configured for `alias`.
18    #[error(
19        "no database configured for alias `{alias}` (set it in the settings, or pass --database-url)"
20    )]
21    NoDatabase {
22        /// The alias that was requested.
23        alias: String,
24    },
25    /// Opening the database `alias` failed.
26    #[error("cannot connect to database `{alias}`: {source}")]
27    Connect {
28        /// The alias that failed.
29        alias: String,
30        /// The connection failure.
31        #[source]
32        source: MigrationError,
33    },
34    /// A migration command failed.
35    #[error(transparent)]
36    Migration(#[from] MigrationError),
37    /// The server failed to start or stopped with an error.
38    #[error(transparent)]
39    Server(#[from] ServerError),
40    /// The OpenAPI document could not be generated.
41    #[error("cannot generate the OpenAPI document: {0}")]
42    OpenApi(String),
43    /// A native database client could not be started.
44    #[error("cannot run `{program}`: {reason}")]
45    Shell {
46        /// Client program name (`psql`, `mysql`, `sqlite3`).
47        program: &'static str,
48        /// Why it could not run.
49        reason: String,
50    },
51    /// Creating a project or invoking cargo failed.
52    #[error("{0}")]
53    Io(String),
54    /// A file would overwrite user content; nothing was written.
55    #[error("{0}")]
56    Conflict(String),
57    /// A live operation did not finish within its bound.
58    #[error("timed out after {after:?} {what}")]
59    Timeout {
60        /// What was being done.
61        what: String,
62        /// The bound that expired.
63        after: std::time::Duration,
64    },
65}
66
67impl CliError {
68    /// Usage error helper.
69    pub fn usage(message: impl Into<String>) -> Self {
70        Self::Usage(message.into())
71    }
72
73    /// Process exit status for this error: `2` for usage errors, else `1`.
74    pub fn exit_code(&self) -> u8 {
75        match self {
76            Self::Usage(_) | Self::Migration(MigrationError::Usage(_)) => 2,
77            _ => 1,
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn usage_errors_exit_with_two() {
88        assert_eq!(CliError::usage("bad").exit_code(), 2);
89        assert_eq!(CliError::from(MigrationError::usage("bad")).exit_code(), 2);
90        assert_eq!(
91            CliError::NoDatabase {
92                alias: "default".into()
93            }
94            .exit_code(),
95            1
96        );
97    }
98}