1use crate::Error;
4use chrono::FixedOffset;
5use serde::{Deserialize, Serialize};
6use std::marker::PhantomData;
7use url::Url;
8
9#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
11#[serde(rename_all = "camelCase")]
12#[must_use]
13pub struct Create {
14 #[serde(rename = "type")]
16 annotation_type: String,
17 datasets: Vec<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 description: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
24 title: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 url: Option<Url>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 time: Option<chrono::DateTime<FixedOffset>>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 end_time: Option<chrono::DateTime<FixedOffset>>,
34}
35
36impl Create {
37 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 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
68pub struct NeedsType;
70pub struct NeedsDatasets;
72pub struct Optionals;
74
75#[derive(PartialEq, Eq, Debug)]
77#[must_use]
78pub struct CreateBuilder<T> {
79 request: Create,
80 _p: PhantomData<T>,
81}
82
83impl CreateBuilder<NeedsType> {
84 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 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 pub fn build(self) -> Create {
131 self.request
132 }
133
134 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 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 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 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 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#[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 pub fn builder() -> ListBuilder {
231 ListBuilder::default()
232 }
233}
234
235#[derive(PartialEq, Eq, Debug, Default)]
237#[must_use]
238pub struct ListBuilder {
239 request: List,
240}
241
242impl ListBuilder {
243 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 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 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 pub fn build(self) -> List {
290 self.request
291 }
292}
293
294#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
296#[serde(rename_all = "camelCase")]
297#[must_use]
298pub struct Update {
299 #[serde(rename = "type")]
301 #[serde(skip_serializing_if = "Option::is_none")]
302 annotation_type: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 datasets: Option<Vec<String>>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 description: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
311 title: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
314 url: Option<Url>,
315 #[serde(skip_serializing_if = "Option::is_none")]
317 time: Option<chrono::DateTime<FixedOffset>>,
318 #[serde(skip_serializing_if = "Option::is_none")]
320 end_time: Option<chrono::DateTime<FixedOffset>>,
321}
322
323impl Update {
324 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#[derive(PartialEq, Eq, Debug)]
342#[must_use]
343pub struct UpdateBuilder {
344 request: Update,
345}
346
347impl UpdateBuilder {
348 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 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 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 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 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 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 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 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}