1use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize, Default)]
9pub struct ApiError {
10 #[serde(default)]
12 pub title: Option<String>,
13 #[serde(default)]
15 pub detail: Option<String>,
16 #[serde(default)]
18 pub status: Option<String>,
19}
20
21impl ApiError {
22 fn message(&self) -> Option<String> {
23 match (&self.title, &self.detail) {
24 (Some(t), Some(d)) if t != d => Some(format!("{t}: {d}")),
25 (Some(t), _) => Some(t.clone()),
26 (_, Some(d)) => Some(d.clone()),
27 _ => None,
28 }
29 }
30}
31
32#[derive(Debug, thiserror::Error)]
34pub enum Error {
35 #[error("transport error: {0}")]
37 Transport(String),
38
39 #[error("decode error: {0}")]
41 Decode(String),
42
43 #[error("invalid request: {0}")]
45 Invalid(String),
46
47 #[error("HackerOne API error (HTTP {status}): {detail}")]
49 Api {
50 status: u16,
52 detail: String,
54 errors: Vec<ApiError>,
56 },
57}
58
59impl Error {
60 pub fn status(&self) -> Option<u16> {
62 match self {
63 Error::Api { status, .. } => Some(*status),
64 _ => None,
65 }
66 }
67
68 pub fn api_errors(&self) -> &[ApiError] {
70 match self {
71 Error::Api { errors, .. } => errors,
72 _ => &[],
73 }
74 }
75}
76
77pub type Result<T> = std::result::Result<T, Error>;
79
80pub(crate) fn from_response(response: &crate::Response) -> Error {
82 let value = response.json().unwrap_or(serde_json::Value::Null);
83 let errors: Vec<ApiError> = value
84 .get("errors")
85 .cloned()
86 .and_then(|v| serde_json::from_value(v).ok())
87 .unwrap_or_default();
88
89 let detail = errors
90 .iter()
91 .filter_map(ApiError::message)
92 .collect::<Vec<_>>()
93 .join("; ");
94
95 let detail = if detail.is_empty() {
96 let text = response.text();
97 if text.trim().is_empty() {
98 "no response body".to_string()
99 } else {
100 text
101 }
102 } else {
103 detail
104 };
105
106 Error::Api {
107 status: response.status,
108 detail,
109 errors,
110 }
111}