1use serde::Serialize;
2use serde_json::Value;
3use std::fmt::{Display, Formatter};
4
5#[derive(Debug, Clone, Copy, Serialize)]
6#[serde(rename_all = "snake_case")]
7pub enum ErrorCategory {
8 Snapshot,
9 Selector,
10 Unsupported,
11 Git,
12 Io,
13 Parse,
14 State,
15}
16
17#[derive(Debug, Clone, Serialize)]
18pub struct AppError {
19 pub code: &'static str,
20 pub message: String,
21 pub category: ErrorCategory,
22 pub retryable: bool,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub details: Option<Value>,
25}
26
27impl AppError {
28 pub fn new(code: &'static str, message: String) -> Self {
29 let (category, retryable) = classify(code);
30 Self {
31 code,
32 message,
33 category,
34 retryable,
35 details: None,
36 }
37 }
38
39 pub fn io(err: std::io::Error) -> Self {
40 Self {
41 code: "io_error",
42 message: err.to_string(),
43 category: ErrorCategory::Io,
44 retryable: false,
45 details: None,
46 }
47 }
48
49 pub fn with_details(mut self, details: Value) -> Self {
50 self.details = Some(details);
51 self
52 }
53
54 pub fn to_json_string(&self) -> String {
55 serde_json::to_string_pretty(&ErrorEnvelope {
56 error: self.clone(),
57 })
58 .expect("error should serialize")
59 }
60}
61
62impl Display for AppError {
63 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}: {}", self.code, self.message)
65 }
66}
67
68impl std::error::Error for AppError {}
69
70pub type AppResult<T> = Result<T, AppError>;
71
72#[derive(Serialize)]
73struct ErrorEnvelope {
74 error: AppError,
75}
76
77fn classify(code: &str) -> (ErrorCategory, bool) {
78 match code {
79 "missing_snapshot" | "stale_snapshot" => (ErrorCategory::Snapshot, true),
80 "invalid_hunk_selector"
81 | "invalid_resolve_range"
82 | "missing_selection"
83 | "unknown_hunk"
84 | "unknown_change"
85 | "unknown_change_key"
86 | "unknown_id"
87 | "unknown_path"
88 | "no_changes_in_path"
89 | "no_resolve_candidates"
90 | "ambiguous_line_range"
91 | "empty_line_range" => (ErrorCategory::Selector, false),
92 "binary_file" | "unsupported_diff" | "empty_diff" | "non_utf8_diff" => {
93 (ErrorCategory::Unsupported, false)
94 }
95 "git_repo_root_failed"
96 | "git_inventory_failed"
97 | "git_diff_failed"
98 | "git_diff_check_failed"
99 | "git_apply_check_failed"
100 | "git_apply_failed"
101 | "git_commit_failed"
102 | "git_rev_parse_failed"
103 | "git_index_path_failed"
104 | "git_read_tree_failed"
105 | "git_diff_name_only_failed"
106 | "git_command_failed" => (ErrorCategory::Git, false),
107 "io_error" | "file_read_failed" | "plan_read_failed" => (ErrorCategory::Io, false),
108 "plan_parse_failed" | "invalid_diff" | "mapping_failed" => (ErrorCategory::Parse, false),
109 _ => (ErrorCategory::State, false),
110 }
111}