use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ExitCode {
Success = 0,
Runtime = 1,
Usage = 2,
NotAStore = 3,
Policy = 4,
Collision = 5,
ValidationFailed = 6,
NotImplemented = 64,
}
impl ExitCode {
pub fn code(self) -> i32 {
self as i32
}
}
#[derive(Debug, Clone)]
pub struct CliError {
pub exit: ExitCode,
pub code: &'static str,
pub message: String,
pub hint: Option<String>,
}
impl CliError {
pub fn new(exit: ExitCode, code: &'static str, message: impl Into<String>) -> Self {
Self {
exit,
code,
message: message.into(),
hint: None,
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
pub fn not_implemented(subcommand: &str) -> Self {
Self::new(
ExitCode::NotImplemented,
"NOT_IMPLEMENTED",
format!("`dbmd {subcommand}` is not implemented yet"),
)
.with_hint("this subcommand is recognized but its body is not implemented in this build")
}
pub fn runtime(message: impl Into<String>) -> Self {
Self::new(ExitCode::Runtime, "RUNTIME_ERROR", message)
}
pub fn to_json(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert(
"code".to_string(),
serde_json::Value::String(self.code.to_string()),
);
obj.insert(
"message".to_string(),
serde_json::Value::String(self.message.clone()),
);
if let Some(hint) = &self.hint {
obj.insert("hint".to_string(), serde_json::Value::String(hint.clone()));
}
serde_json::json!({ "error": serde_json::Value::Object(obj) })
}
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
if let Some(hint) = &self.hint {
write!(f, "\n hint: {hint}")?;
}
Ok(())
}
}
impl std::error::Error for CliError {}
impl From<dbmd_core::Error> for CliError {
fn from(err: dbmd_core::Error) -> Self {
match err {
dbmd_core::Error::NotAStore(_) => {
CliError::new(ExitCode::NotAStore, "NOT_A_STORE", err.to_string())
.with_hint("run `dbmd` from inside a db.md store, or pass the store path")
}
dbmd_core::Error::Policy { code, message } => {
CliError::new(ExitCode::Policy, code, message)
}
dbmd_core::Error::Store(_) => {
CliError::new(ExitCode::Runtime, "STORE_ERROR", err.to_string())
}
dbmd_core::Error::Parse(_) => {
CliError::new(ExitCode::Runtime, "PARSE_ERROR", err.to_string())
}
dbmd_core::Error::Io(_) => {
CliError::new(ExitCode::Runtime, "IO_ERROR", err.to_string())
}
}
}
}
impl From<std::io::Error> for CliError {
fn from(err: std::io::Error) -> Self {
CliError::new(ExitCode::Runtime, "IO_ERROR", err.to_string())
}
}
impl From<dbmd_core::linkmd::LinkError> for CliError {
fn from(err: dbmd_core::linkmd::LinkError) -> Self {
use dbmd_core::linkmd::LinkError as L;
let message = err.to_string();
match err {
L::NoHub => CliError::new(ExitCode::Runtime, "NO_HUB", message),
L::NoCredential => CliError::new(ExitCode::Runtime, "NO_CREDENTIAL", message),
L::BadKey => CliError::new(ExitCode::Runtime, "BAD_CREDENTIAL", message),
L::UnsafeHub { .. } => CliError::new(ExitCode::Runtime, "HUB_NOT_HTTPS", message),
L::Transport { .. } => CliError::new(ExitCode::Runtime, "HUB_UNREACHABLE", message),
L::Http { code, .. } => {
let e = CliError::new(ExitCode::Runtime, "HUB_ERROR", message);
match code {
Some(c) => e.with_hint(format!("hub error code: {c}")),
None => e,
}
}
L::NotJson { .. } => CliError::new(ExitCode::Runtime, "HUB_NOT_JSON", message),
L::ResponseTooLarge => CliError::new(ExitCode::Runtime, "RESPONSE_TOO_LARGE", message),
L::BadAddress { .. } => CliError::new(ExitCode::Runtime, "BAD_ADDRESS", message)
.with_hint(
"addresses are `@brain`, `@brain/<record-id>`, or `@brain/<store-path>.md`",
),
L::BadGrantId { .. } => CliError::new(ExitCode::Runtime, "BAD_GRANT_ID", message)
.with_hint("copy the id from `dbmd grant list <brain>`"),
L::UnsafePath { .. } => CliError::new(ExitCode::Runtime, "UNSAFE_PATH", message),
L::PushTooLarge { .. } => CliError::new(ExitCode::Runtime, "PUSH_TOO_LARGE", message),
L::ProposeTooLarge { .. } => {
CliError::new(ExitCode::Runtime, "PROPOSE_TOO_LARGE", message)
}
L::NotUtf8 { .. } => CliError::new(ExitCode::Runtime, "NOT_UTF8", message),
L::InvalidPack { .. } => CliError::new(ExitCode::Runtime, "INVALID_PACK", message),
L::InvalidFeed { .. } => CliError::new(ExitCode::Runtime, "INVALID_FEED", message),
L::Io(_) => CliError::new(ExitCode::Runtime, "IO_ERROR", message),
L::Store(_) => CliError::new(ExitCode::Runtime, "STORE_ERROR", message),
}
}
}
pub type CliResult = std::result::Result<(), CliError>;