Skip to main content

git_hunk/
error.rs

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}