1use std::collections::HashMap;
2use std::fmt;
3use std::time::Duration;
4
5use serde::Deserialize;
6
7use crate::client::{BoxError, HttpResponse};
8
9#[derive(Debug)]
11#[non_exhaustive]
12pub enum Error {
13 Api(ApiError),
15 Transport(BoxError),
17 Json(serde_json::Error),
20 InsecureBaseUrl,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub struct ApiError {
36 pub status: u16,
38 pub code: String,
40 pub message: String,
42 pub fields: HashMap<String, Vec<String>>,
44 pub retry_after: Option<Duration>,
46}
47
48#[derive(Deserialize, Default)]
49struct ErrorBody {
50 error: Option<String>,
51 message: Option<String>,
52 fields: Option<HashMap<String, Vec<String>>>,
53}
54
55impl ApiError {
56 pub(crate) fn from_response(response: &HttpResponse) -> Self {
59 let body: ErrorBody = serde_json::from_slice(&response.body).unwrap_or_default();
60 Self {
61 status: response.status,
62 code: body.error.unwrap_or_default(),
63 message: body.message.unwrap_or_default(),
64 fields: body.fields.unwrap_or_default(),
65 retry_after: response
66 .header("retry-after")
67 .and_then(|value| value.trim().parse::<u64>().ok())
68 .map(Duration::from_secs),
69 }
70 }
71}
72
73impl fmt::Display for ApiError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 if self.message.is_empty() {
76 write!(f, "alpr: {} {}", self.status, self.code)
77 } else {
78 write!(f, "alpr: {} {}: {}", self.status, self.code, self.message)
79 }
80 }
81}
82
83impl std::error::Error for ApiError {}
84
85impl fmt::Display for Error {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Error::Api(error) => error.fmt(f),
89 Error::Transport(error) => write!(f, "alpr: {error}"),
90 Error::Json(error) => write!(f, "alpr: json: {error}"),
91 Error::InsecureBaseUrl => f.write_str("alpr: the base URL must be https (http is accepted only for localhost, 127.0.0.0/8 and ::1), so the API key is never sent in clear text"),
92 }
93 }
94}
95
96impl std::error::Error for Error {
97 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98 match self {
99 Error::Api(error) => Some(error),
100 Error::Transport(error) => Some(error.as_ref()),
101 Error::Json(error) => Some(error),
102 Error::InsecureBaseUrl => None,
103 }
104 }
105}
106
107impl From<ApiError> for Error {
108 fn from(error: ApiError) -> Self {
109 Error::Api(error)
110 }
111}
112
113impl Error {
114 pub fn api(&self) -> Option<&ApiError> {
116 match self {
117 Error::Api(error) => Some(error),
118 _ => None,
119 }
120 }
121}