1use std::fmt;
2
3use jev_protocol::{ApiError, ProtocolError};
4
5use crate::TransportError;
6
7/// Why a question went unanswered. Every variant that saw a response
8/// carries its request id, the only handle for a billing dispute.
9#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11pub enum ClientError {
12    /// The request would break an API rule; nothing was sent.
13    Invalid(ProtocolError),
14    /// The API refused, after any retries. The request id is inside.
15    Api { error: ApiError, attempts: u32 },
16    /// No usable response. `request_id` is the last one any earlier
17    /// attempt received.
18    Transport { error: TransportError, attempts: u32, request_id: Option<String> },
19    /// Every attempt ran out of time, or the budget did.
20    TimedOut { attempts: u32, request_id: Option<String> },
21    /// A 200 that does not answer the questions, or answers from another
22    /// model.
23    Response { error: ProtocolError, request_id: Option<String> },
24}
25
26impl ClientError {
27    /// The request id to quote, if any response carried one.
28    pub fn request_id(&self) -> Option<&str> {
29        match self {
30            ClientError::Invalid(_) => None,
31            ClientError::Api { error, .. } => error.request_id.as_deref(),
32            ClientError::Transport { request_id, .. }
33            | ClientError::TimedOut { request_id, .. }
34            | ClientError::Response { request_id, .. } => request_id.as_deref(),
35        }
36    }
37}
38
39impl fmt::Display for ClientError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        let attempts = |n: &u32| if *n == 1 { String::new() } else { format!(" after {n} attempts") };
42        match self {
43            ClientError::Invalid(e) => write!(f, "{e}"),
44            // ApiError's own Display carries the request id.
45            ClientError::Api { error, attempts: n } => write!(f, "{error}{}", attempts(n)),
46            ClientError::Transport { error, attempts: n, .. } => write!(f, "{error}{}", attempts(n)),
47            ClientError::TimedOut { attempts: n, .. } => write!(f, "Jev did not answer in time{}", attempts(n)),
48            ClientError::Response { error, .. } => write!(f, "{error}"),
49        }?;
50        match self {
51            ClientError::Transport { request_id: Some(id), .. }
52            | ClientError::TimedOut { request_id: Some(id), .. }
53            | ClientError::Response { request_id: Some(id), .. } => write!(f, " [request {id}]"),
54            _ => Ok(()),
55        }
56    }
57}
58
59impl std::error::Error for ClientError {}