axiom_rs/annotations/
requests.rs

1//! Request types for the annotations API.
2
3use crate::Error;
4use chrono::FixedOffset;
5use serde::{Deserialize, Serialize};
6use std::marker::PhantomData;
7use url::Url;
8
9/// A request to create an annotation.
10#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
11#[serde(rename_all = "camelCase")]
12#[must_use]
13pub struct Create {
14    /// Type of the event marked by the annotation. Use only alphanumeric characters or hyphens. For example, "production-deployment".
15    #[serde(rename = "type")]
16    annotation_type: String,
17    /// Dataset names for which the annotation appears on charts
18    datasets: Vec<String>,
19    /// Explanation of the event the annotation marks on the charts
20    #[serde(skip_serializing_if = "Option::is_none")]
21    description: Option<String>,
22    /// Summary of the annotation that appears on the charts
23    #[serde(skip_serializing_if = "Option::is_none")]
24    title: Option<String>,
25    /// URL relevant for the event marked by the annotation. For example, link to GitHub pull request.
26    #[serde(skip_serializing_if = "Option::is_none")]
27    url: Option<Url>,
28    /// Time the annotation marks on the charts. If you don't include this field, Axiom assigns the time of the API request to the annotation.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    time: Option<chrono::DateTime<FixedOffset>>,
31    ///End time of the annotation
32    #[serde(skip_serializing_if = "Option::is_none")]
33    end_time: Option<chrono::DateTime<FixedOffset>>,
34}
35
36impl Create {
37    /// New annotation builder.
38    pub fn builder() -> CreateBuilder<NeedsType> {
39        CreateBuilder {
40            request: Create {
41                annotation_type: String::new(),
42                datasets: Vec::new(),
43                description: None,
44                title: None,
45                url: None,
46                time: None,
47                end_time: None,
48            },
49            _p: PhantomData,
50        }
51    }
52    /// Helper to quickly create a simple annotation request with just a `type` and `datasets`.
53    ///
54    /// # Errors
55    /// If the datasets are empty.
56    /// If the annotation type is empty.
57    pub fn new(
58        annotation_type: &(impl ToString + ?Sized),
59        datasets: Vec<String>,
60    ) -> Result<Self, Error> {
61        Ok(Create::builder()
62            .with_type(annotation_type)?
63            .with_datasets(datasets)?
64            .build())
65    }
66}
67
68/// The builder needs an annotation type to be set.
69pub struct NeedsType;
70/// The builder needs datasets to be set.
71pub struct NeedsDatasets;
72/// The builder is ready to build the request but optional fields can still be set.
73pub struct Optionals;
74
75/// A builder for creating an annotation request.
76#[derive(PartialEq, Eq, Debug)]
77#[must_use]
78pub struct CreateBuilder<T> {
79    request: Create,
80    _p: PhantomData<T>,
81}
82
83impl CreateBuilder<NeedsType> {
84    /// Set the type of the annotation.
85    ///
86    /// Type of the event marked by the annotation. Use only alphanumeric characters or hyphens.
87    /// For example, "production-deployment".
88    ///
89    /// # Errors
90    /// If the type is empty.
91    pub fn with_type(
92        self,
93        annotation_type: &(impl ToString + ?Sized),
94    ) -> Result<CreateBuilder<NeedsDatasets>, Error> {
95        let annotation_type = annotation_type.to_string();
96        if annotation_type.is_empty() {
97            return Err(Error::EmptyType);
98        }
99        Ok(CreateBuilder {
100            request: Create {
101                annotation_type,
102                ..self.request
103            },
104            _p: PhantomData,
105        })
106    }
107}
108
109impl CreateBuilder<NeedsDatasets> {
110    /// Set the datasets for which the annotation appears on charts.
111    ///
112    /// # Errors
113    /// If the datasets are empty.
114    pub fn with_datasets(self, datasets: Vec<String>) -> Result<CreateBuilder<Optionals>, Error> {
115        if datasets.is_empty() {
116            return Err(Error::EmptyDatasets);
117        }
118        Ok(CreateBuilder {
119            request: Create {
120                datasets,
121                ..self.request
122            },
123            _p: PhantomData,
124        })
125    }
126}
127
128impl CreateBuilder<Optionals> {
129    /// Builds the request
130    pub fn build(self) -> Create {
131        self.request
132    }
133
134    /// Set the description of the annotation.
135    ///
136    /// Explanation of the event the annotation marks on the charts.
137    pub fn with_description(self, description: &(impl ToString + ?Sized)) -> Self {
138        Self {
139            request: Create {
140                description: Some(description.to_string()),
141                ..self.request
142            },
143            _p: PhantomData,
144        }
145    }
146
147    /// Set the title of the annotation.
148    ///
149    /// Summary of the annotation that appears on the charts
150    pub fn with_title(self, title: &(impl ToString + ?Sized)) -> Self {
151        Self {
152            request: Create {
153                title: Some(title.to_string()),
154                ..self.request
155            },
156            _p: PhantomData,
157        }
158    }
159
160    /// Set the URL of the annotation.
161    ///
162    /// URL relevant for the event marked by the annotation. For example, link to GitHub pull request.
163    pub fn with_url(self, url: Url) -> Self {
164        Self {
165            request: Create {
166                url: Some(url),
167                ..self.request
168            },
169            _p: PhantomData,
170        }
171    }
172
173    /// Set the (start) time of the annotation.
174    ///
175    /// Time the annotation marks on the charts. If you don't include this field,
176    /// Axiom assigns the time of the API request to the annotation.
177    ///
178    /// # Errors
179    /// If the start time is after the end time.
180    pub fn with_time(self, time: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
181        if let Some(end_time) = self.request.end_time {
182            if time > end_time {
183                return Err(Error::InvalidTimeOrder);
184            }
185        }
186        Ok(Self {
187            request: Create {
188                time: Some(time),
189                ..self.request
190            },
191            _p: PhantomData,
192        })
193    }
194
195    /// Set the end time of the annotation.
196    ///
197    /// # Errors
198    /// If the start time is after the end time.
199    pub fn with_end_time(self, end_time: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
200        if let Some(time) = self.request.time {
201            if time > end_time {
202                return Err(Error::InvalidTimeOrder);
203            }
204        }
205        Ok(Self {
206            request: Create {
207                end_time: Some(end_time),
208                ..self.request
209            },
210            _p: PhantomData,
211        })
212    }
213}
214
215#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default)]
216#[serde(rename_all = "camelCase")]
217/// A request to all annotations
218#[must_use]
219pub struct List {
220    #[serde(skip_serializing_if = "Option::is_none")]
221    datasets: Option<Vec<String>>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    start: Option<chrono::DateTime<FixedOffset>>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    end: Option<chrono::DateTime<FixedOffset>>,
226}
227
228impl List {
229    /// New list request builder.
230    pub fn builder() -> ListBuilder {
231        ListBuilder::default()
232    }
233}
234
235/// A builder for creating a list request.
236#[derive(PartialEq, Eq, Debug, Default)]
237#[must_use]
238pub struct ListBuilder {
239    request: List,
240}
241
242impl ListBuilder {
243    /// Set the datasets for which the annotations are listed.
244    pub fn with_datasets(self, datasets: Vec<String>) -> Self {
245        Self {
246            request: List {
247                datasets: Some(datasets),
248                ..self.request
249            },
250        }
251    }
252
253    /// Set the start time of the list.
254    ///
255    /// # Errors
256    /// If the start time is after the end time.
257    pub fn with_start(self, start: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
258        if let Some(end) = self.request.end {
259            if start > end {
260                return Err(Error::InvalidTimeOrder);
261            }
262        }
263        Ok(Self {
264            request: List {
265                start: Some(start),
266                ..self.request
267            },
268        })
269    }
270
271    /// Set the end time of list.
272    ///
273    /// # Errors
274    /// If the start time is after the end time.
275    pub fn with_end(self, end: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
276        if let Some(start) = self.request.start {
277            if start > end {
278                return Err(Error::InvalidTimeOrder);
279            }
280        }
281        Ok(Self {
282            request: List {
283                end: Some(end),
284                ..self.request
285            },
286        })
287    }
288    /// Builds the request
289    pub fn build(self) -> List {
290        self.request
291    }
292}
293
294/// A request to update an annotation.
295#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
296#[serde(rename_all = "camelCase")]
297#[must_use]
298pub struct Update {
299    /// Type of the event marked by the annotation. Use only alphanumeric characters or hyphens. For example, "production-deployment".
300    #[serde(rename = "type")]
301    #[serde(skip_serializing_if = "Option::is_none")]
302    annotation_type: Option<String>,
303    /// Dataset names for which the annotation appears on charts
304    #[serde(skip_serializing_if = "Option::is_none")]
305    datasets: Option<Vec<String>>,
306    /// Explanation of the event the annotation marks on the charts
307    #[serde(skip_serializing_if = "Option::is_none")]
308    description: Option<String>,
309    /// Summary of the annotation that appears on the charts
310    #[serde(skip_serializing_if = "Option::is_none")]
311    title: Option<String>,
312    /// URL relevant for the event marked by the annotation. For example, link to GitHub pull request.
313    #[serde(skip_serializing_if = "Option::is_none")]
314    url: Option<Url>,
315    /// Time the annotation marks on the charts. If you don't include this field, Axiom assigns the time of the API request to the annotation.
316    #[serde(skip_serializing_if = "Option::is_none")]
317    time: Option<chrono::DateTime<FixedOffset>>,
318    ///End time of the annotation
319    #[serde(skip_serializing_if = "Option::is_none")]
320    end_time: Option<chrono::DateTime<FixedOffset>>,
321}
322
323impl Update {
324    /// New update builder.
325    pub fn builder() -> UpdateBuilder {
326        UpdateBuilder {
327            request: Update {
328                annotation_type: None,
329                datasets: None,
330                description: None,
331                title: None,
332                url: None,
333                time: None,
334                end_time: None,
335            },
336        }
337    }
338}
339
340/// A builder for creating an annotation request.
341#[derive(PartialEq, Eq, Debug)]
342#[must_use]
343pub struct UpdateBuilder {
344    request: Update,
345}
346
347impl UpdateBuilder {
348    /// Builds the request
349    ///
350    /// # Errors
351    /// If the request is empty.
352    pub fn build(self) -> Result<Update, Error> {
353        let request = self.request;
354        if request.annotation_type.is_none()
355            && request.datasets.is_none()
356            && request.description.is_none()
357            && request.title.is_none()
358            && request.url.is_none()
359            && request.time.is_none()
360            && request.end_time.is_none()
361        {
362            return Err(Error::EmptyUpdate);
363        }
364        Ok(request)
365    }
366
367    /// Set the type of the annotation.
368    ///
369    /// Type of the event marked by the annotation. Use only alphanumeric characters or hyphens.
370    /// For example, "production-deployment".
371    ///
372    /// # Errors
373    /// If the type is empty.
374    pub fn with_type(self, annotation_type: &(impl ToString + ?Sized)) -> Result<Self, Error> {
375        let annotation_type = annotation_type.to_string();
376        if annotation_type.is_empty() {
377            return Err(Error::EmptyType);
378        }
379        Ok(UpdateBuilder {
380            request: Update {
381                annotation_type: Some(annotation_type),
382                ..self.request
383            },
384        })
385    }
386
387    /// Set the datasets for which the annotation appears on charts.
388    pub fn with_datasets(self, datasets: Vec<String>) -> Self {
389        UpdateBuilder {
390            request: Update {
391                datasets: Some(datasets),
392                ..self.request
393            },
394        }
395    }
396
397    /// Set the description of the annotation.
398    ///
399    /// Explanation of the event the annotation marks on the charts.
400    pub fn with_description(self, description: &(impl ToString + ?Sized)) -> Self {
401        Self {
402            request: Update {
403                description: Some(description.to_string()),
404                ..self.request
405            },
406        }
407    }
408
409    /// Set the title of the annotation.
410    ///
411    /// Summary of the annotation that appears on the charts
412    pub fn with_title(self, title: &(impl ToString + ?Sized)) -> Self {
413        Self {
414            request: Update {
415                title: Some(title.to_string()),
416                ..self.request
417            },
418        }
419    }
420
421    /// Set the URL of the annotation.
422    ///
423    /// URL relevant for the event marked by the annotation. For example, link to GitHub pull request.
424    pub fn with_url(self, url: Url) -> Self {
425        Self {
426            request: Update {
427                url: Some(url),
428                ..self.request
429            },
430        }
431    }
432
433    /// Set the (start) time of the annotation.
434    ///
435    /// Time the annotation marks on the charts. If you don't include this field,
436    /// Axiom assigns the time of the API request to the annotation.
437    ///
438    /// # Errors
439    /// If the start time is after the end time.
440    pub fn with_time(self, time: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
441        if let Some(end_time) = self.request.end_time {
442            if time > end_time {
443                return Err(Error::InvalidTimeOrder);
444            }
445        }
446        Ok(Self {
447            request: Update {
448                time: Some(time),
449                ..self.request
450            },
451        })
452    }
453
454    /// Set the end time of the annotation.
455    ///
456    /// # Errors
457    /// If the start time is after the end time.
458    pub fn with_end_time(self, end_time: chrono::DateTime<FixedOffset>) -> Result<Self, Error> {
459        if let Some(time) = self.request.time {
460            if time > end_time {
461                return Err(Error::InvalidTimeOrder);
462            }
463        }
464        Ok(Self {
465            request: Update {
466                end_time: Some(end_time),
467                ..self.request
468            },
469        })
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    #[test]
476    fn empty_datasets() {
477        let res = super::Create::new("snot", vec![]);
478        assert!(matches!(res, Err(super::Error::EmptyDatasets)));
479
480        let res = super::Create::builder()
481            .with_type("snot")
482            .expect("we got type")
483            .with_datasets(vec![]);
484        assert!(matches!(res, Err(super::Error::EmptyDatasets)));
485    }
486    #[test]
487    fn create_invalid_times() {
488        let start = chrono::DateTime::parse_from_rfc3339("2024-02-06T11:39:28.382Z")
489            .expect("the time is right");
490        let end = chrono::DateTime::parse_from_rfc3339("2023-02-06T11:39:28.382Z")
491            .expect("the time is right");
492        let res = super::Create::builder()
493            .with_type("snot")
494            .expect("we got type")
495            .with_datasets(vec!["badger".to_string()])
496            .expect("we got datasets")
497            .with_time(start)
498            .expect("we got time")
499            .with_end_time(end);
500        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
501        let res = super::Create::builder()
502            .with_type("snot")
503            .expect("we got type")
504            .with_datasets(vec!["badger".to_string()])
505            .expect("we got datasets")
506            .with_end_time(end)
507            .expect("we got time")
508            .with_time(start);
509        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
510    }
511
512    #[test]
513    fn list_invalid_times() {
514        let start = chrono::DateTime::parse_from_rfc3339("2024-02-06T11:39:28.382Z")
515            .expect("the time is right");
516        let end = chrono::DateTime::parse_from_rfc3339("2023-02-06T11:39:28.382Z")
517            .expect("the time is right");
518        let res = super::List::builder()
519            .with_start(start)
520            .expect("we got start")
521            .with_end(end);
522        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
523        let res = super::List::builder()
524            .with_end(end)
525            .expect("we got start")
526            .with_start(start);
527        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
528    }
529
530    #[test]
531    fn update_invalid_times() {
532        let start = chrono::DateTime::parse_from_rfc3339("2024-02-06T11:39:28.382Z")
533            .expect("the time is right");
534        let end = chrono::DateTime::parse_from_rfc3339("2023-02-06T11:39:28.382Z")
535            .expect("the time is right");
536        let res = super::Update::builder()
537            .with_time(start)
538            .expect("we got start")
539            .with_end_time(end);
540        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
541        let res = super::Update::builder()
542            .with_end_time(end)
543            .expect("we got start")
544            .with_time(start);
545        assert!(matches!(res, Err(super::Error::InvalidTimeOrder)));
546    }
547}