use std::fmt;
#[derive(Debug)]
pub enum CliError {
AppFolderNotFound {
path: String,
},
StateFileNotFound {
path: String,
},
ServeDirNotFound {
path: String,
},
PortInUse {
port: u16,
},
EntryReadFailed {
path: String,
},
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::AppFolderNotFound { path } => write!(f, "App folder not found: {path}"),
CliError::StateFileNotFound { path } => write!(f, "State file not found: {path}"),
CliError::ServeDirNotFound { path } => write!(f, "Serve directory not found: {path}"),
CliError::PortInUse { port } => {
write!(f, "Port {port} on 127.0.0.1 is already in use")
}
CliError::EntryReadFailed { path } => write!(f, "Failed to read entry file: {path}"),
}
}
}
impl std::error::Error for CliError {}
impl CliError {
#[must_use]
pub fn hint(&self) -> &'static str {
match self {
CliError::AppFolderNotFound { .. } => "Check that the app folder path exists",
CliError::StateFileNotFound { .. } => "Pass a valid --state path to a JSON file",
CliError::ServeDirNotFound { .. } => "Pass a valid --servedir path for static assets",
CliError::PortInUse { .. } => {
"Stop the process using that port, or rerun with --port <free-port>. Previous dev \
sessions may have left a server running."
}
CliError::EntryReadFailed { .. } => {
"Use --entry <file> to specify a different entry file"
}
}
}
#[must_use]
pub fn exit_code(&self) -> u8 {
match self {
CliError::AppFolderNotFound { .. }
| CliError::StateFileNotFound { .. }
| CliError::ServeDirNotFound { .. }
| CliError::EntryReadFailed { .. } => 66,
CliError::PortInUse { .. } => 69,
}
}
}
#[must_use]
pub fn exit_code(err: &anyhow::Error) -> i32 {
if let Some(cli) = err.chain().find_map(|c| c.downcast_ref::<CliError>()) {
return i32::from(cli.exit_code());
}
if let Some(web) = err
.chain()
.find_map(|c| c.downcast_ref::<webui::WebUIError>())
{
return match web {
webui::WebUIError::Parse { .. }
| webui::WebUIError::Serialization(_)
| webui::WebUIError::InvalidBuildOptions(_)
| webui::WebUIError::ComponentAssets(_)
| webui::WebUIError::Projection(_) => 65,
webui::WebUIError::Io { .. } => 74,
_ => 1,
};
}
if err.chain().any(|c| c.is::<std::io::Error>()) {
return 74;
}
1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_error_hints_and_codes() {
let missing = CliError::AppFolderNotFound { path: "x".into() };
assert_eq!(missing.exit_code(), 66);
assert!(missing.hint().contains("app folder"));
let port = CliError::PortInUse { port: 3000 };
assert_eq!(port.exit_code(), 69);
assert!(port.hint().contains("--port"));
}
#[test]
fn exit_code_classifies_cli_error_through_chain() {
let err = anyhow::Error::new(CliError::StateFileNotFound {
path: "s.json".into(),
})
.context("while starting the dev server");
assert_eq!(exit_code(&err), 66);
}
#[test]
fn exit_code_for_parse_error_is_dataerr() {
let err: anyhow::Error = webui::WebUIError::InvalidBuildOptions("bad".into()).into();
assert_eq!(exit_code(&err), 65);
}
#[test]
fn exit_code_for_plain_io_error_is_ioerr() {
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "nope");
let err = anyhow::Error::new(io).context("reading file");
assert_eq!(exit_code(&err), 74);
}
#[test]
fn exit_code_default_is_one() {
let err = anyhow::anyhow!("something generic");
assert_eq!(exit_code(&err), 1);
}
}