Skip to main content

ferraus_core/
error.rs

1//! Error type shared by every ferraus crate, with stable exit codes.
2
3use serde::Serialize;
4use std::fmt;
5
6/// Error categories. The discriminant is the process exit code (see the PRD exit-code table).
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ErrorKind {
10    /// Processing failed.
11    Processing,
12    /// Usage or parameter error.
13    Usage,
14    /// Input missing or unreadable.
15    InputMissing,
16    /// Unsupported format or codec.
17    Unsupported,
18    /// Model not cached and downloads disabled.
19    ModelNotCached,
20    /// Model download or verification failed.
21    ModelDownload,
22    /// Plugin crashed, timed out, or was not found.
23    Plugin,
24    /// Cancelled (including timeouts and memory limits).
25    Cancelled,
26    /// Output verification failed.
27    Verification,
28    /// Network error.
29    Network,
30    /// Internal error (bug).
31    Internal,
32}
33
34impl ErrorKind {
35    pub fn exit_code(self) -> i32 {
36        match self {
37            ErrorKind::Processing => 1,
38            ErrorKind::Usage => 2,
39            ErrorKind::InputMissing => 3,
40            ErrorKind::Unsupported => 4,
41            ErrorKind::ModelNotCached => 5,
42            ErrorKind::ModelDownload => 6,
43            ErrorKind::Plugin => 7,
44            ErrorKind::Cancelled => 8,
45            ErrorKind::Verification => 9,
46            ErrorKind::Network => 10,
47            ErrorKind::Internal => 70,
48        }
49    }
50
51    pub fn all() -> &'static [ErrorKind] {
52        &[
53            ErrorKind::Processing,
54            ErrorKind::Usage,
55            ErrorKind::InputMissing,
56            ErrorKind::Unsupported,
57            ErrorKind::ModelNotCached,
58            ErrorKind::ModelDownload,
59            ErrorKind::Plugin,
60            ErrorKind::Cancelled,
61            ErrorKind::Verification,
62            ErrorKind::Network,
63            ErrorKind::Internal,
64        ]
65    }
66
67    pub fn describe(self) -> &'static str {
68        match self {
69            ErrorKind::Processing => "Processing failed",
70            ErrorKind::Usage => "Usage or parameter error",
71            ErrorKind::InputMissing => "Input missing or unreadable",
72            ErrorKind::Unsupported => "Unsupported format or codec",
73            ErrorKind::ModelNotCached => "Model not cached and downloads disabled",
74            ErrorKind::ModelDownload => "Model download or verification failed",
75            ErrorKind::Plugin => "Plugin crashed, timed out, or was not found",
76            ErrorKind::Cancelled => "Cancelled",
77            ErrorKind::Verification => "Output verification failed (checksum, clipping guard, duration mismatch)",
78            ErrorKind::Network => "Network error",
79            ErrorKind::Internal => "Internal error (bug)",
80        }
81    }
82}
83
84/// The error carried through every command. `code` is a stable snake_case identifier,
85/// `hint` is a runnable command when one exists.
86#[derive(Debug, Clone, Serialize)]
87pub struct Error {
88    #[serde(skip)]
89    pub kind: ErrorKind,
90    pub code: String,
91    pub message: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub hint: Option<String>,
94    pub exit_code: i32,
95}
96
97impl Error {
98    pub fn new(kind: ErrorKind, code: impl Into<String>, message: impl Into<String>) -> Self {
99        Error { kind, code: code.into(), message: message.into(), hint: None, exit_code: kind.exit_code() }
100    }
101
102    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
103        self.hint = Some(hint.into());
104        self
105    }
106
107    pub fn usage(message: impl Into<String>) -> Self {
108        Error::new(ErrorKind::Usage, "invalid_argument", message)
109    }
110
111    pub fn processing(message: impl Into<String>) -> Self {
112        Error::new(ErrorKind::Processing, "processing_failed", message)
113    }
114
115    pub fn input(message: impl Into<String>) -> Self {
116        Error::new(ErrorKind::InputMissing, "input_unreadable", message)
117    }
118
119    pub fn unsupported(message: impl Into<String>) -> Self {
120        Error::new(ErrorKind::Unsupported, "unsupported_format", message)
121    }
122
123    pub fn internal(message: impl Into<String>) -> Self {
124        Error::new(ErrorKind::Internal, "internal_error", message).with_hint(
125            "Please report this at https://github.com/djynnius/ferraus/issues with the command and its sidecar",
126        )
127    }
128
129    pub fn verification(message: impl Into<String>) -> Self {
130        Error::new(ErrorKind::Verification, "verification_failed", message)
131    }
132
133    pub fn plugin(message: impl Into<String>) -> Self {
134        Error::new(ErrorKind::Plugin, "plugin_failed", message)
135    }
136
137    pub fn cancelled(message: impl Into<String>) -> Self {
138        Error::new(ErrorKind::Cancelled, "cancelled", message)
139    }
140}
141
142impl fmt::Display for Error {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{}", self.message)?;
145        if let Some(h) = &self.hint {
146            write!(f, " (hint: {h})")?;
147        }
148        Ok(())
149    }
150}
151
152impl std::error::Error for Error {}
153
154impl From<std::io::Error> for Error {
155    fn from(e: std::io::Error) -> Self {
156        match e.kind() {
157            std::io::ErrorKind::NotFound => Error::new(ErrorKind::InputMissing, "not_found", e.to_string()),
158            std::io::ErrorKind::PermissionDenied => {
159                Error::new(ErrorKind::InputMissing, "permission_denied", e.to_string())
160            }
161            _ => Error::new(ErrorKind::Processing, "io_error", e.to_string()),
162        }
163    }
164}
165
166impl From<serde_json::Error> for Error {
167    fn from(e: serde_json::Error) -> Self {
168        Error::usage(format!("invalid JSON: {e}"))
169    }
170}
171
172pub type Result<T, E = Error> = std::result::Result<T, E>;