1#![deny(missing_docs)]
3
4use chrono::{DateTime, Utc};
8use lazy_regex::regex_captures;
9use log::{debug, error, trace};
10use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue, AUTHORIZATION};
11use reqwest::{Client, Url};
12use serde::{Deserialize, Serialize};
13use serde_json::{Map, Value};
14use std::borrow::Cow;
15use std::collections::HashSet;
16use std::fmt::Display;
17use std::str::FromStr;
18use url::ParseError;
19use uuid::Uuid;
20
21#[derive(Debug, Clone)]
25pub struct Hook0Client {
26 client: Client,
27 api_url: Url,
28 application_id: Uuid,
29}
30
31impl Hook0Client {
32 pub fn new(api_url: Url, application_id: Uuid, token: &str) -> Result<Self, Hook0ClientError> {
38 let authenticated_client = HeaderValue::from_str(&format!("Bearer {token}"))
39 .map_err(|e| Hook0ClientError::AuthHeader(e).log_and_return())
40 .map(|hv| HeaderMap::from_iter([(AUTHORIZATION, hv)]))
41 .and_then(|headers| {
42 Client::builder()
43 .default_headers(headers)
44 .build()
45 .map_err(|e| Hook0ClientError::ReqwestClient(e).log_and_return())
46 })?;
47
48 Ok(Self {
49 api_url,
50 client: authenticated_client,
51 application_id,
52 })
53 }
54
55 pub fn api_url(&self) -> &Url {
57 &self.api_url
58 }
59
60 pub fn application_id(&self) -> &Uuid {
62 &self.application_id
63 }
64
65 fn mk_url(&self, segments: &[&str]) -> Result<Url, Hook0ClientError> {
66 append_url_segments(&self.api_url, segments)
67 .map_err(|e| Hook0ClientError::Url(e).log_and_return())
68 }
69
70 pub async fn send_event(&self, event: &Event<'_>) -> Result<Uuid, Hook0ClientError> {
72 let event_ingestion_url = self.mk_url(&["event"])?;
73 let full_event = FullEvent::from_event(event, &self.application_id);
74
75 let res = self
76 .client
77 .post(event_ingestion_url)
78 .json(&full_event)
79 .send()
80 .await
81 .map_err(|e| {
82 Hook0ClientError::EventSending {
83 event_id: full_event.event_id.to_owned(),
84 error: e,
85 body: None,
86 }
87 .log_and_return()
88 })?;
89
90 match res.error_for_status_ref() {
91 Ok(_) => Ok(full_event.event_id),
92 Err(e) => {
93 let body = res.text().await.ok();
94 Err(Hook0ClientError::EventSending {
95 event_id: full_event.event_id.to_owned(),
96 error: e,
97 body,
98 }
99 .log_and_return())
100 }
101 }
102 }
103
104 pub async fn upsert_event_types(
108 &self,
109 event_types: &[&str],
110 ) -> Result<Vec<String>, Hook0ClientError> {
111 let structured_event_types = event_types
112 .iter()
113 .map(|str| {
114 EventType::from_str(str)
115 .map_err(|_| Hook0ClientError::InvalidEventType(str.to_string()))
116 })
117 .collect::<Result<Vec<EventType>, Hook0ClientError>>()?;
118
119 let event_types_url = self.mk_url(&["event_types"])?;
120 #[derive(Debug, Deserialize)]
121 struct ApiEventType {
122 event_type_name: String,
123 }
124
125 trace!("Getting the list of available event types");
126 let available_event_types_vec = self
127 .client
128 .get(event_types_url.as_str())
129 .query(&[("application_id", self.application_id())])
130 .send()
131 .await
132 .map_err(Hook0ClientError::GetAvailableEventTypes)?
133 .error_for_status()
134 .map_err(Hook0ClientError::GetAvailableEventTypes)?
135 .json::<Vec<ApiEventType>>()
136 .await
137 .map_err(Hook0ClientError::GetAvailableEventTypes)?;
138 let available_event_types = available_event_types_vec
139 .iter()
140 .map(|et| et.event_type_name.to_owned())
141 .collect::<HashSet<String>>();
142 debug!(
143 "There are currently {} event types",
144 available_event_types.len(),
145 );
146
147 #[derive(Debug, Serialize)]
148 struct ApiEventTypePost {
149 application_id: Uuid,
150 service: String,
151 resource_type: String,
152 verb: String,
153 }
154 impl Display for ApiEventTypePost {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{}.{}.{}", self.service, self.resource_type, self.verb)
157 }
158 }
159
160 let mut added_event_types = vec![];
161 for event_type in structured_event_types {
162 let event_type_str = event_type.to_string();
163 if !available_event_types.contains(&event_type_str) {
164 debug!("Creating the '{event_type}' event type");
165
166 let body = ApiEventTypePost {
167 application_id: self.application_id,
168 service: event_type.service,
169 resource_type: event_type.resource_type,
170 verb: event_type.verb,
171 };
172
173 self.client
174 .post(event_types_url.as_str())
175 .json(&body)
176 .send()
177 .await
178 .map_err(|e| Hook0ClientError::CreatingEventType {
179 event_type_name: body.to_string(),
180 error: e,
181 })?
182 .error_for_status()
183 .map_err(|e| Hook0ClientError::CreatingEventType {
184 event_type_name: body.to_string(),
185 error: e,
186 })?;
187
188 added_event_types.push(body.to_string());
189 }
190 }
191 debug!("{} new event types were created", added_event_types.len());
192
193 Ok(added_event_types)
194 }
195}
196
197#[derive(Debug, Serialize, PartialEq, Eq)]
199pub struct EventType {
200 service: String,
201 resource_type: String,
202 verb: String,
203}
204
205impl FromStr for EventType {
206 type Err = ();
207
208 fn from_str(s: &str) -> Result<Self, Self::Err> {
209 let captures = regex_captures!("^([A-Z0-9_]+)[.]([A-Z0-9_]+)[.]([A-Z0-9_]+)$"i, s);
210 if let Some((_, service, resource_type, verb)) = captures {
211 Ok(Self {
212 resource_type: resource_type.to_owned(),
213 service: service.to_owned(),
214 verb: verb.to_owned(),
215 })
216 } else {
217 Err(())
218 }
219 }
220}
221
222impl Display for EventType {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}.{}.{}", self.service, self.resource_type, self.verb)
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct Event<'a> {
231 pub event_id: &'a Option<&'a Uuid>,
233 pub event_type: &'a str,
235 pub payload: Cow<'a, str>,
237 pub payload_content_type: &'a str,
239 pub metadata: Option<Vec<(String, Value)>>,
241 pub occurred_at: Option<DateTime<Utc>>,
243 pub labels: Vec<(String, Value)>,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
248struct FullEvent<'a> {
249 pub application_id: Uuid,
250 pub event_id: Uuid,
251 pub event_type: &'a str,
252 pub payload: &'a str,
253 pub payload_content_type: &'a str,
254 pub metadata: Option<Map<String, Value>>,
255 pub occurred_at: DateTime<Utc>,
256 pub labels: Map<String, Value>,
257}
258
259impl<'a> FullEvent<'a> {
260 pub fn from_event(event: &'a Event, application_id: &Uuid) -> Self {
261 let event_id = event
262 .event_id
263 .map(|uuid| uuid.to_owned())
264 .unwrap_or_else(Uuid::new_v4);
265 let occurred_at = event.occurred_at.unwrap_or_else(Utc::now);
266
267 Self {
268 application_id: application_id.to_owned(),
269 event_id,
270 event_type: event.event_type,
271 payload: event.payload.as_ref(),
272 payload_content_type: event.payload_content_type,
273 metadata: event
274 .metadata
275 .as_ref()
276 .map(|items| Map::from_iter(items.iter().cloned())),
277 occurred_at,
278 labels: Map::from_iter(event.labels.iter().cloned()),
279 }
280 }
281}
282
283#[derive(Debug, thiserror::Error)]
285pub enum Hook0ClientError {
286 #[error("Could not build auth header: {0}")]
290 AuthHeader(InvalidHeaderValue),
291
292 #[error("Could not build reqwest HTTP client: {0}")]
296 ReqwestClient(reqwest::Error),
297
298 #[error("Could not create a valid URL to request Hook0's API: {0}")]
302 Url(ParseError),
303
304 #[error("Sending event {event_id} failed: {error} [body={}]", body.as_deref().unwrap_or(""))]
306 EventSending {
307 event_id: Uuid,
309
310 error: reqwest::Error,
312
313 body: Option<String>,
315 },
316
317 #[error("Provided event type '{0}' does not have a valid syntax (service.resource_type.verb)")]
319 InvalidEventType(String),
320
321 #[error("Getting available event types failed: {0}")]
323 GetAvailableEventTypes(reqwest::Error),
324
325 #[error("Creating event type '{event_type_name}' failed: {error}")]
327 CreatingEventType {
328 event_type_name: String,
330
331 error: reqwest::Error,
333 },
334}
335
336impl Hook0ClientError {
337 pub fn log_and_return(self) -> Self {
339 error!("{self}");
340 self
341 }
342}
343
344fn append_url_segments(base_url: &Url, segments: &[&str]) -> Result<Url, url::ParseError> {
345 const SEP: &str = "/";
346 let segments_str = segments.join(SEP);
347
348 let url = Url::parse(&format!("{base_url}/{segments_str}").replace("//", "/"))?;
349
350 Ok(url)
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn displaying_event_type() {
359 let et = EventType {
360 service: "service".to_owned(),
361 resource_type: "resource".to_owned(),
362 verb: "verb".to_owned(),
363 };
364
365 assert_eq!(et.to_string(), "service.resource.verb")
366 }
367
368 #[test]
369 fn parsing_valid_event_type() {
370 let et = EventType {
371 service: "service".to_owned(),
372 resource_type: "resource".to_owned(),
373 verb: "verb".to_owned(),
374 };
375
376 assert_eq!(EventType::from_str(&et.to_string()), Ok(et))
377 }
378
379 #[test]
380 fn parsing_invalid_event_type() {
381 assert_eq!(EventType::from_str("test.test"), Err(()))
382 }
383}