eiktyrner/
error.rs

1use std::{cmp::min, fmt};
2
3#[derive(Debug, thiserror::Error)]
4pub enum Error {
5    #[error("Http error: {0}")]
6    Http(#[from] http::Error),
7
8    #[error("Network error: {0}")]
9    Net(#[from] hyper::Error),
10
11    #[error("Invalid body: {0}")]
12    InvalidBody(String),
13
14    #[error("Error serializing request payload: {0}")]
15    Serialization(serde_json::Error),
16
17    #[error("Error deserializing response: {error}\n{extract}")]
18    Deserialization {
19        error: serde_json::Error,
20        extract: ErrorExtract,
21    },
22
23    #[error("Received a non 2xx status code: StatusCode: {received_status}, body: {body}")]
24    Non2xxResponse {
25        received_status: http::StatusCode,
26        body: String,
27    },
28}
29
30#[derive(Debug)]
31pub enum ErrorExtract {
32    Failed,
33    Extracted { extract: String, pointer_idx: usize },
34}
35
36impl fmt::Display for ErrorExtract {
37    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38        if let Self::Extracted {
39            extract,
40            pointer_idx,
41        } = self
42        {
43            writeln!(f, "...{}...", extract)?;
44
45            for _ in 0..pointer_idx + 3 {
46                f.write_str(" ")?;
47            }
48            f.write_str("^")?;
49        }
50
51        Ok(())
52    }
53}
54
55impl Error {
56    pub fn invalid_body(s: impl Into<String>) -> Self {
57        Self::InvalidBody(s.into())
58    }
59
60    pub fn deserialization(error: serde_json::Error, body: &[u8]) -> Self {
61        // Try to take an extract from the body.
62        let column = error.column();
63        let extract = body
64            .split(|b| b == &b'\n')
65            .nth(error.line() - 1) // subtract 1 since indices start at 1.
66            .map_or_else(
67                || ErrorExtract::Failed,
68                |line| {
69                    const GRASP: usize = 800;
70
71                    let pointer_idx = min(column, GRASP);
72                    let start = if GRASP < column {
73                        column - GRASP
74                    } else {
75                        column
76                    };
77                    let snippet_bs = &line[start..min(column + GRASP, line.len())];
78                    ErrorExtract::Extracted {
79                        extract: String::from_utf8_lossy(snippet_bs).into_owned(),
80                        pointer_idx,
81                    }
82                },
83            );
84
85        Self::Deserialization { error, extract }
86    }
87
88    pub fn non_2xx(received_status: http::StatusCode, body: &[u8]) -> Self {
89        Self::Non2xxResponse {
90            received_status,
91            body: Self::sanitize_body(body),
92        }
93    }
94
95    fn sanitize_body(body: &[u8]) -> String {
96        let mut res = body
97            .chunks(128)
98            .next()
99            .map(|bs| String::from_utf8_lossy(bs).to_string())
100            .unwrap_or_else(|| "".to_owned());
101
102        res.push_str("...");
103        res
104    }
105}