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