Skip to main content

alpr/
error.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::time::Duration;
4
5use serde::Deserialize;
6
7use crate::client::{BoxError, HttpResponse};
8
9/// What a call answers instead of its resource.
10#[derive(Debug)]
11#[non_exhaustive]
12pub enum Error {
13    /// The API refused the request.
14    Api(ApiError),
15    /// The request did not reach the API, or its answer did not arrive.
16    Transport(BoxError),
17    /// A body could not be written as JSON, or the API answered something
18    /// this SDK does not read.
19    Json(serde_json::Error),
20    /// The client's base URL is not https and not a loopback address, so no
21    /// request was sent: the API key would cross the network in clear text.
22    InsecureBaseUrl,
23}
24
25/// A request the API refused.
26///
27/// `code` is the API's: invalid_api_key, insufficient_scope,
28/// insufficient_credits, missing image, invalid_image, invalid_option,
29/// image_too_large, no_plate, not_found, image_unavailable, rate_limited,
30/// engine_unavailable or storage_unavailable. `fields` says, when the API
31/// names them, what is wrong with each field (empty otherwise).
32/// `retry_after` is how long to wait on a 429.
33#[derive(Debug, Clone, PartialEq, Eq)]
34#[non_exhaustive]
35pub struct ApiError {
36    /// The HTTP status.
37    pub status: u16,
38    /// The API's code for the refusal.
39    pub code: String,
40    /// What went wrong, for a person to read.
41    pub message: String,
42    /// The problems of each field of the request, when the API names them.
43    pub fields: HashMap<String, Vec<String>>,
44    /// How long to wait before trying again.
45    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    /// The error a refused response carries. A refusal without a JSON body is
57    /// still a refusal.
58    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    /// The refusal, when the API refused the request.
115    pub fn api(&self) -> Option<&ApiError> {
116        match self {
117            Error::Api(error) => Some(error),
118            _ => None,
119        }
120    }
121}