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    /// Whether the failed ask certainly cost nothing: it was never sent, or
28    /// the API refused the one attempt made. Anything else (a retried
29    /// refusal, a broken exchange, a timeout, an unusable 200) may have been
30    /// billed, and a budget should count it as spent.
31    pub fn nothing_billed(&self) -> bool {
32        match self {
33            ClientError::Invalid(_) => true,
34            ClientError::Api { attempts, .. } => *attempts == 1,
35            _ => false,
36        }
37    }
38
39    /// The request id to quote, if any response carried one.
40    pub fn request_id(&self) -> Option<&str> {
41        match self {
42            ClientError::Invalid(_) => None,
43            ClientError::Api { error, .. } => error.request_id.as_deref(),
44            ClientError::Transport { request_id, .. }
45            | ClientError::TimedOut { request_id, .. }
46            | ClientError::Response { request_id, .. } => request_id.as_deref(),
47        }
48    }
49}
50
51impl fmt::Display for ClientError {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        let attempts = |n: &u32| if *n == 1 { String::new() } else { format!(" after {n} attempts") };
54        match self {
55            ClientError::Invalid(e) => write!(f, "{e}"),
56            // ApiError's own Display carries the request id.
57            ClientError::Api { error, attempts: n } => write!(f, "{error}{}", attempts(n)),
58            ClientError::Transport { error, attempts: n, .. } => write!(f, "{error}{}", attempts(n)),
59            ClientError::TimedOut { attempts: n, .. } => write!(f, "Jev did not answer in time{}", attempts(n)),
60            ClientError::Response { error, .. } => write!(f, "{error}"),
61        }?;
62        match self {
63            ClientError::Transport { request_id: Some(id), .. }
64            | ClientError::TimedOut { request_id: Some(id), .. }
65            | ClientError::Response { request_id: Some(id), .. } => write!(f, " [request {id}]"),
66            _ => Ok(()),
67        }
68    }
69}
70
71impl std::error::Error for ClientError {}