1use std::fmt;
2
3use jev_protocol::{ApiError, ProtocolError};
4
5use crate::TransportError;

Why a question went unanswered. Every variant that saw a response carries its request id, the only handle for a billing dispute.

9#[derive(Clone, Debug, PartialEq)]
10#[non_exhaustive]
11pub enum ClientError {

The request would break an API rule; nothing was sent.

13    Invalid(ProtocolError),

The API refused, after any retries. The request id is inside.

15    Api { error: ApiError, attempts: u32 },

No usable response. request_id is the last one any earlier attempt received.

18    Transport { error: TransportError, attempts: u32, request_id: Option<String> },

Every attempt ran out of time, or the budget did.

20    TimedOut { attempts: u32, request_id: Option<String> },

A 200 that does not answer the questions, or answers from another model.

23    Response { error: ProtocolError, request_id: Option<String> },
24}
26impl ClientError {

Whether the failed ask certainly cost nothing: it was never sent, or the API refused the one attempt made. Anything else (a retried refusal, a broken exchange, a timeout, an unusable 200) may have been 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    }

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}
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 {}