1use error_support::{error, ErrorHandling, GetErrorHandling};
7use viaduct::Response;
8
9pub type ApiResult<T> = std::result::Result<T, ApiError>;
10
11#[derive(Debug, thiserror::Error, uniffi::Error)]
12pub enum ApiError {
13 #[error("Something unexpected occurred.")]
14 Other { reason: String },
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum ComponentError {
19 #[error("Error requesting ads: {0}")]
20 RequestAds(#[from] RequestAdsError),
21
22 #[error("Error recording a click for a placement: {0}")]
23 RecordClick(#[from] RecordClickError),
24
25 #[error("Error recording an impressions for a placement: {0}")]
26 RecordImpression(#[from] RecordImpressionError),
27
28 #[error("Error reporting an ad: {0}")]
29 ReportAd(#[from] ReportAdError),
30}
31
32impl GetErrorHandling for ComponentError {
33 type ExternalError = ApiError;
34
35 fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
36 ErrorHandling::convert(ApiError::Other {
37 reason: self.to_string(),
38 })
39 }
40}
41
42#[derive(Debug, thiserror::Error)]
43pub enum RequestAdsError {
44 #[error("Error building ad requests from configs: {0}")]
45 BuildRequest(#[from] BuildRequestError),
46
47 #[error("Error requesting ads from MARS: {0}")]
48 FetchAds(#[from] FetchAdsError),
49
50 #[error("Error building placements from ad response: {0}")]
51 BuildPlacements(#[from] BuildPlacementsError),
52}
53
54#[derive(Debug, thiserror::Error)]
55pub enum BuildRequestError {
56 #[error("Could not build request with empty placement configs")]
57 EmptyConfig,
58
59 #[error("Duplicate placement_id found: {placement_id}. Placement_ids must be unique.")]
60 DuplicatePlacementId { placement_id: String },
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum BuildPlacementsError {
65 #[error("Duplicate placement_id found: {placement_id}. Placement_ids must be unique.")]
66 DuplicatePlacementId { placement_id: String },
67}
68
69#[derive(Debug, thiserror::Error)]
70pub enum FetchAdsError {
71 #[error("URL parse error: {0}")]
72 UrlParse(#[from] url::ParseError),
73
74 #[error("Error sending request: {0}")]
75 Request(#[from] viaduct::Error),
76
77 #[error("JSON error: {0}")]
78 Json(#[from] serde_json::Error),
79
80 #[error("Could not fetch ads, MARS responded with: {0}")]
81 HTTPError(#[from] HTTPError),
82}
83
84#[derive(Debug, thiserror::Error)]
85pub enum EmitTelemetryError {
86 #[error("URL parse error: {0}")]
87 UrlParse(#[from] url::ParseError),
88
89 #[error("Error sending request: {0}")]
90 Request(#[from] viaduct::Error),
91
92 #[error("JSON error: {0}")]
93 Json(#[from] serde_json::Error),
94
95 #[error("Could not fetch ads, MARS responded with: {0}")]
96 HTTPError(#[from] HTTPError),
97}
98
99#[derive(Debug, thiserror::Error)]
100pub enum CallbackRequestError {
101 #[error("URL parse error: {0}")]
102 UrlParse(#[from] url::ParseError),
103
104 #[error("Error sending request: {0}")]
105 Request(#[from] viaduct::Error),
106
107 #[error("JSON error: {0}")]
108 Json(#[from] serde_json::Error),
109
110 #[error("Could not fetch ads, MARS responded with: {0}")]
111 HTTPError(#[from] HTTPError),
112
113 #[error("Callback URL missing: {message}")]
114 MissingCallback { message: String },
115}
116
117#[derive(Debug, thiserror::Error)]
118pub enum RecordImpressionError {
119 #[error("Callback request to MARS failed: {0}")]
120 CallbackRequest(#[from] CallbackRequestError),
121}
122
123#[derive(Debug, thiserror::Error)]
124pub enum RecordClickError {
125 #[error("Callback request to MARS failed: {0}")]
126 CallbackRequest(#[from] CallbackRequestError),
127}
128
129#[derive(Debug, thiserror::Error)]
130pub enum ReportAdError {
131 #[error("Callback request to MARS failed: {0}")]
132 CallbackRequest(#[from] CallbackRequestError),
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum HTTPError {
137 #[error("Bad request ({code}): {message}")]
138 BadRequest { code: u16, message: String },
139
140 #[error("Server error ({code}): {message}")]
141 Server { code: u16, message: String },
142
143 #[error("Unexpected error ({code}): {message}")]
144 Unexpected { code: u16, message: String },
145}
146
147pub fn check_http_status_for_error(response: &Response) -> Result<(), HTTPError> {
148 let status = response.status;
149
150 if status == 200 {
151 return Ok(());
152 }
153 let error_message = response.text();
154 let error = match status {
155 400 => HTTPError::BadRequest {
156 code: status,
157 message: error_message.to_string(),
158 },
159 500..=599 => HTTPError::Server {
160 code: status,
161 message: error_message.to_string(),
162 },
163 _ => HTTPError::Unexpected {
164 code: status,
165 message: error_message.to_string(),
166 },
167 };
168 Err(error)
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use url::Url;
175
176 fn mock_response(status: u16, body: &str) -> Response {
177 Response {
178 request_method: viaduct::Method::Get,
179 url: Url::parse("https://example.com").unwrap(),
180 status,
181 headers: viaduct::Headers::new(),
182 body: body.as_bytes().to_vec(),
183 }
184 }
185
186 #[test]
187 fn test_ok_status_returns_ok() {
188 let response = mock_response(200, "OK");
189 let result = check_http_status_for_error(&response);
190 assert!(result.is_ok());
191 }
192
193 #[test]
194 fn test_bad_request_returns_http_error() {
195 let response = mock_response(400, "Bad input");
196 let result = check_http_status_for_error(&response);
197 assert!(
198 matches!(result, Err(HTTPError::BadRequest { code, message }) if code == 400 && message == "Bad input")
199 );
200 }
201
202 #[test]
203 fn test_server_error_500() {
204 let response = mock_response(500, "Something broke");
205 let result = check_http_status_for_error(&response);
206 assert!(
207 matches!(result, Err(HTTPError::Server { code, message }) if code == 500 && message == "Something broke")
208 );
209 }
210}