freedom_api/
api.rs

1//! # ATLAS Freedom API
2//!
3//! This module exists to define the Freedom API trait, which can be implemented for multiple client
4//! types.
5//!
6//! The API trait
7#![allow(clippy::type_complexity)]
8use std::{future::Future, ops::Deref, pin::Pin};
9
10use async_stream::stream;
11use bytes::Bytes;
12use freedom_config::Config;
13use freedom_models::{
14    account::Account,
15    band::Band,
16    pagination::Paginated,
17    satellite::Satellite,
18    satellite_configuration::SatelliteConfiguration,
19    site::Site,
20    task::{Task, TaskRequest, TaskStatusType, TaskType},
21    user::User,
22    utils::Embedded,
23};
24use reqwest::{Response, StatusCode};
25use serde::de::DeserializeOwned;
26use serde_json::Value as JsonValue;
27use time::{format_description::well_known::Iso8601, OffsetDateTime};
28use url::Url;
29
30use futures_core::Stream;
31
32use crate::error::Error;
33
34pub(crate) mod post;
35
36/// A super trait containing all the requirements for Freedom API Values
37pub trait Value: std::fmt::Debug + DeserializeOwned + Clone + Send + Sync {}
38
39impl<T> Value for T where T: std::fmt::Debug + DeserializeOwned + Clone + Send + Sync {}
40
41trait PaginatedErr<'a, T> {
42    fn once_err(self) -> PaginatedStream<'a, T>;
43}
44
45impl<'a, T: 'a + Send + Sync> PaginatedErr<'a, T> for Error {
46    fn once_err(self) -> PaginatedStream<'a, T> {
47        Box::pin(async_stream::stream! { yield Err(self); })
48    }
49}
50
51/// The trait defining the required functionality of container types
52///
53/// The Freedom API is generic over "containers". Each implementer of the [`Api`] trait must
54/// also define a container. This is useful since certain clients will return Arc'd values, i.e. the
55/// caching client, while others return the values wrapped in a simple [`Inner`] type which is just
56/// a stack value.
57///
58/// However, for most cases this complexity can be ignored, since containers are required to
59/// implement [`Deref`](std::ops::Deref) of `T`. So for read-only operations the container can be
60/// used as if it were `T`. For mutable access see [`Self::into_inner`].
61///
62/// # Example
63///
64/// ```no_run
65/// # use freedom_api::prelude::*;
66/// # #[tokio::main]
67/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
68/// # let config = Config::from_env()?;
69/// # let client = Client::from_config(config);
70/// let request = client
71///     .get_request_by_id(42)
72///     .await?;
73///
74/// println!("Created on {}", request.created); // Direct access to created field
75///                                             // through the Container
76/// # Ok(())
77/// # }
78/// ```
79pub trait Container<T>: Deref<Target = T> + Value {
80    /// All containers are capable of returning the value they wrap
81    ///
82    /// However, the runtime performance of this varies by client type. For [`crate::Client`], this
83    /// operation is essentially free, however for the caching client, this often results in a clone
84    /// of the value.
85    fn into_inner(self) -> T;
86}
87
88impl<T: Deref<Target = T> + Value> Container<T> for Box<T> {
89    fn into_inner(self) -> T {
90        *self
91    }
92}
93
94/// A simple container which stores a `T`.
95///
96/// This container exists to allow us to store items on the stack, without needing to allocate with
97/// something like `Box<T>`. For all other intents and purposes, it acts as the `T` which it
98/// contains.
99#[derive(
100    Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
101)]
102#[repr(transparent)]
103#[serde(transparent)]
104pub struct Inner<T>(T);
105
106impl<T> std::ops::Deref for Inner<T> {
107    type Target = T;
108
109    fn deref(&self) -> &Self::Target {
110        &self.0
111    }
112}
113
114impl<T> std::ops::DerefMut for Inner<T> {
115    fn deref_mut(&mut self) -> &mut Self::Target {
116        &mut self.0
117    }
118}
119
120impl<T> Container<T> for Inner<T>
121where
122    T: Value,
123{
124    fn into_inner(self) -> T {
125        self.0
126    }
127}
128
129impl<T> Inner<T> {
130    pub fn new(inner: T) -> Self {
131        Self(inner)
132    }
133}
134
135/// A stream of paginated results from freedom.
136///
137/// Each item in the stream is a result, since one or more items may fail to be serialized
138pub type PaginatedStream<'a, T> = Pin<Box<dyn Stream<Item = Result<T, Error>> + 'a + Send + Sync>>;
139
140/// The primary trait for interfacing with the Freedom API
141pub trait Api: Send + Sync {
142    /// The [`Api`] supports implementors with different so-called "container" types.
143    ///
144    /// For a more detailed description, see the [`Container`] trait.
145    type Container<T: Value>: Container<T>;
146
147    /// Creates a get request at the provided absolute URI for the client's environment, using basic
148    /// authentication.
149    ///
150    /// The JSON response is then deserialized into the required type, erroring if the
151    /// deserialization fails, and providing the object if it succeeds.
152    fn get_json_map<T>(&self, url: Url) -> impl Future<Output = Result<T, Error>> + Send + Sync
153    where
154        T: Value,
155    {
156        async move {
157            let (body, status) = self.get(url).await?;
158
159            error_on_non_success(&status)?;
160
161            let utf8_str = String::from_utf8_lossy(&body);
162            serde_json::from_str(&utf8_str).map_err(From::from)
163        }
164    }
165
166    /// Creates a get request at the provided absolute URI for the client's environment, using basic
167    /// authentication.
168    ///
169    /// Returns the raw binary body, and the status code.
170    fn get(
171        &self,
172        url: Url,
173    ) -> impl Future<Output = Result<(Bytes, StatusCode), Error>> + Send + Sync;
174
175    /// Creates a stream of items from a paginated endpoint.
176    ///
177    /// The stream is produced as a collection of `Result<T>`. This is so that if any one item fails
178    /// deserialization, it is added to the stream of items as an error rather than causing the
179    /// entire stream to result in an Error.
180    ///
181    /// # Pinning
182    ///
183    /// For convenience the stream is pinned on the heap via [`Box::pin`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.pin).
184    /// This allows us to treat the returned stream more like any other object, without requiring
185    /// the end user to manually  pin the result on the stack. This comes with a slight performance
186    /// penalty (it requires an allocation), however this will be negligible given the latency of
187    /// the responses. For more information on pinning in rust refer to the [pinning chapter](https://rust-lang.github.io/async-book/04_pinning/01_chapter.html)
188    /// of the async book.
189    fn get_paginated<T>(&self, head_url: Url) -> PaginatedStream<'_, Self::Container<T>>
190    where
191        T: 'static + Value + Send + Sync,
192    {
193        let base = self.config().environment().freedom_entrypoint();
194        let mut current_url = head_url; // Not necessary but makes control flow more obvious
195        Box::pin(stream! {
196            loop {
197                // Get the results for the current page.
198                let pag = self.get_json_map::<Paginated<JsonValue>>(current_url).await?;
199                for item in pag.items {
200                    let i = serde_json::from_value::<Self::Container<T>>(item).map_err(From::from);
201                    yield i;
202                }
203                if let Some(link) = pag.links.get("next") {
204                    // Update the URL to the next page.
205                    current_url = match link.has_host() {
206                        true => link.to_owned(),
207                        false => {
208                            base.clone()
209                                .join(link.as_str())
210                                .map_err(|e| crate::error::Error::pag_item(e.to_string()))?
211                        }
212                    };
213                } else {
214                    break;
215                }
216            }
217        })
218    }
219
220    /// Returns the freedom configuration for the API
221    fn config(&self) -> &Config;
222
223    /// Returns a mutable reference to the freedom configuration for the API
224    fn config_mut(&mut self) -> &mut Config;
225
226    /// Fetch the URL from the given path
227    ///
228    /// # Panics
229    ///
230    /// Panics in the event the URL cannot be constructed from the provided path
231    fn path_to_url(&self, path: impl AsRef<str>) -> Url {
232        let url = self.config().environment().freedom_entrypoint();
233        url.join(path.as_ref()).expect("Invalid URL construction")
234    }
235
236    fn delete(&self, url: Url) -> impl Future<Output = Result<Response, Error>> + Send;
237
238    /// Request to delete the band details object matching the provided id
239    ///
240    /// # Example
241    ///
242    /// ```no_run
243    /// # use freedom_api::prelude::*;
244    /// # tokio_test::block_on(async {
245    /// let client = Client::from_env()?;
246    ///
247    /// client.delete_task_request(42).await?;
248    /// # Ok::<_, Box<dyn std::error::Error>>(())
249    /// # });
250    /// ```
251    fn delete_band_details(&self, id: i32) -> impl Future<Output = Result<Response, Error>> + Send {
252        async move {
253            let uri = self.path_to_url(format!("satellite_bands/{id}"));
254            self.delete(uri).await
255        }
256    }
257
258    /// Request to delete the satellite configuration matching the provided `id`
259    ///
260    /// # Example
261    ///
262    /// ```no_run
263    /// # use freedom_api::prelude::*;
264    /// # tokio_test::block_on(async {
265    /// let client = Client::from_env()?;
266    ///
267    /// client.delete_satellite_configuration(42).await?;
268    /// # Ok::<_, Box<dyn std::error::Error>>(())
269    /// # });
270    /// ```
271    fn delete_satellite_configuration(
272        &self,
273        id: i32,
274    ) -> impl Future<Output = Result<Response, Error>> + Send {
275        async move {
276            let uri = self.path_to_url(format!("satellite_configurations/{id}"));
277            self.delete(uri).await
278        }
279    }
280
281    /// Request to delete the satellite object matching the provided `id`
282    ///
283    /// # Example
284    ///
285    /// ```no_run
286    /// # use freedom_api::prelude::*;
287    /// # tokio_test::block_on(async {
288    /// let client = Client::from_env()?;
289    ///
290    /// client.delete_satellite(42).await?;
291    /// # Ok::<_, Box<dyn std::error::Error>>(())
292    /// # });
293    /// ```
294    fn delete_satellite(&self, id: i32) -> impl Future<Output = Result<Response, Error>> + Send {
295        async move {
296            let uri = self.path_to_url(format!("satellites/{id}"));
297            self.delete(uri).await
298        }
299    }
300
301    /// Request to delete the override matching the provided `id`
302    ///
303    /// # Example
304    ///
305    /// ```no_run
306    /// # use freedom_api::prelude::*;
307    /// # tokio_test::block_on(async {
308    /// let client = Client::from_env()?;
309    ///
310    /// client.delete_override(42).await?;
311    /// # Ok::<_, Box<dyn std::error::Error>>(())
312    /// # });
313    /// ```
314    fn delete_override(&self, id: i32) -> impl Future<Output = Result<Response, Error>> + Send {
315        async move {
316            let uri = self.path_to_url(format!("overrides/{id}"));
317            self.delete(uri).await
318        }
319    }
320
321    /// Request to delete the user matching the provided `id`
322    ///
323    /// # Example
324    ///
325    /// ```no_run
326    /// # use freedom_api::prelude::*;
327    /// # tokio_test::block_on(async {
328    /// let client = Client::from_env()?;
329    ///
330    /// client.delete_user(42).await?;
331    /// # Ok::<_, Box<dyn std::error::Error>>(())
332    /// # });
333    /// ```
334    fn delete_user(&self, id: i32) -> impl Future<Output = Result<Response, Error>> + Send {
335        async move {
336            let uri = self.path_to_url(format!("users/{id}"));
337            self.delete(uri).await
338        }
339    }
340
341    /// Request to delete the user matching the provided `id`
342    ///
343    /// # Example
344    ///
345    /// ```no_run
346    /// # use freedom_api::prelude::*;
347    /// # tokio_test::block_on(async {
348    /// let client = Client::from_env()?;
349    ///
350    /// client.delete_task_request(42).await?;
351    /// # Ok::<_, Box<dyn std::error::Error>>(())
352    /// # });
353    /// ```
354    fn delete_task_request(&self, id: i32) -> impl Future<Output = Result<Response, Error>> + Send {
355        async move {
356            let uri = self.path_to_url(format!("requests/{id}"));
357            self.delete(uri).await
358        }
359    }
360
361    /// Lower level method, not intended for direct use
362    fn post_deserialize<S, T>(
363        &self,
364        url: Url,
365        msg: S,
366    ) -> impl Future<Output = Result<T, Error>> + Send + Sync
367    where
368        S: serde::Serialize + Send + Sync,
369        T: Value,
370    {
371        async move {
372            let resp = self.post(url, msg).await?;
373
374            resp.json::<T>().await.map_err(From::from)
375        }
376    }
377
378    /// Lower level method, not intended for direct use
379    fn post<S>(
380        &self,
381        url: Url,
382        msg: S,
383    ) -> impl Future<Output = Result<Response, Error>> + Send + Sync
384    where
385        S: serde::Serialize + Send + Sync;
386
387    /// Produces a single [`Account`](freedom_models::account::Account) matching the provided ID.
388    ///
389    /// See [`get`](Self::get) documentation for more details about the process and return type
390    ///
391    /// # Example
392    ///
393    /// ```no_run
394    /// # use freedom_api::prelude::*;
395    /// # tokio_test::block_on(async {
396    /// let client = Client::from_env()?;
397    ///
398    /// let account = client.get_account_by_name("ATLAS").await?;
399    /// println!("{}", account.name);
400    /// # Ok::<_, Box<dyn std::error::Error>>(())
401    /// # });
402    /// ```
403    fn get_account_by_name(
404        &self,
405        account_name: &str,
406    ) -> impl Future<Output = Result<Self::Container<Account>, Error>> + Send + Sync {
407        async move {
408            let mut uri = self.path_to_url("accounts/search/findOneByName");
409            uri.set_query(Some(&format!("name={account_name}")));
410            self.get_json_map(uri).await
411        }
412    }
413
414    /// Produces a single [`Account`](freedom_models::account::Account) matching the provided ID.
415    ///
416    /// See [`get`](Self::get) documentation for more details about the process and return type
417    ///
418    /// # Example
419    ///
420    /// ```no_run
421    /// # use freedom_api::prelude::*;
422    /// # tokio_test::block_on(async {
423    /// let client = Client::from_env()?;
424    ///
425    /// let data = client.get_file_by_task_id_and_name(42, "data.bin").await?;
426    /// # Ok::<_, Box<dyn std::error::Error>>(())
427    /// # });
428    /// ```
429    fn get_file_by_task_id_and_name(
430        &self,
431        task_id: i32,
432        file_name: &str,
433    ) -> impl Future<Output = Result<Bytes, Error>> + Send + Sync {
434        async move {
435            let path = format!("downloads/{}/{}", task_id, file_name);
436            let uri = self.path_to_url(path);
437
438            let (data, status) = self.get(uri).await?;
439            error_on_non_success(&status)?;
440
441            Ok(data)
442        }
443    }
444
445    /// Produces a single [`Account`](freedom_models::account::Account) matching the provided ID.
446    ///
447    /// See [`get`](Self::get) documentation for more details about the process and return type
448    fn get_account_by_id(
449        &self,
450        account_id: i32,
451    ) -> impl Future<Output = Result<Self::Container<Account>, Error>> + Send + Sync {
452        async move {
453            let uri = self.path_to_url(format!("accounts/{account_id}"));
454            self.get_json_map(uri).await
455        }
456    }
457
458    /// Produces a paginated stream of [`Account`](freedom_models::account::Account) objects.
459    ///
460    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
461    /// and return type
462    fn get_accounts(&self) -> PaginatedStream<'_, Self::Container<Account>> {
463        let uri = self.path_to_url("accounts");
464        self.get_paginated(uri)
465    }
466
467    /// Produces a paginated stream of [`Band`] objects.
468    ///
469    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
470    /// and return type
471    fn get_satellite_bands(&self) -> PaginatedStream<'_, Self::Container<Band>> {
472        let uri = self.path_to_url("satellite_bands");
473        self.get_paginated(uri)
474    }
475
476    /// Produces a single [`Band`] matching the provided ID.
477    ///
478    /// See [`get`](Self::get) documentation for more details about the process and return type
479    fn get_satellite_band_by_id(
480        &self,
481        satellite_band_id: i32,
482    ) -> impl Future<Output = Result<Self::Container<Band>, Error>> + Send + Sync {
483        async move {
484            let uri = self.path_to_url(format!("satellite_bands/{satellite_band_id}"));
485            self.get_json_map(uri).await
486        }
487    }
488
489    /// Produces a single [`Band`] matching the provided name.
490    ///
491    /// See [`get`](Self::get) documentation for more details about the process and return type
492    fn get_satellite_band_by_name(
493        &self,
494        satellite_band_name: &str,
495    ) -> impl Future<Output = Result<Self::Container<Band>, Error>> + Send + Sync {
496        async move {
497            let mut uri = self.path_to_url("satellite_bands/search/findOneByName");
498            uri.set_query(Some(&format!("name={satellite_band_name}")));
499            self.get_json_map(uri).await
500        }
501    }
502
503    /// Produces a paginated stream of [`Band`] objects matching the provided account name.
504    ///
505    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
506    /// and return type
507    fn get_satellite_bands_by_account_name(
508        &self,
509        account_name: &str,
510    ) -> PaginatedStream<'_, Self::Container<Band>> {
511        let mut uri = self.path_to_url("satellite_bands/search/findAllByAccountName");
512        uri.set_query(Some(&format!("accountName={account_name}")));
513
514        self.get_paginated(uri)
515    }
516
517    /// Produces a paginated stream of [`SatelliteConfiguration`] objects matching the provided
518    /// account name.
519    ///
520    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
521    /// and return type
522    fn get_satellite_configurations_by_account_name(
523        &self,
524        account_name: &str,
525    ) -> PaginatedStream<'_, Self::Container<SatelliteConfiguration>> {
526        let mut uri = self.path_to_url("satellite_configurations/search/findAllByAccountName");
527        uri.set_query(Some(&format!("accountName={account_name}")));
528
529        self.get_paginated(uri)
530    }
531
532    /// Produces a paginated stream of [`SatelliteConfiguration`] objects.
533    ///
534    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
535    /// and return type
536    fn get_satellite_configurations(
537        &self,
538    ) -> PaginatedStream<'_, Self::Container<SatelliteConfiguration>> {
539        let uri = self.path_to_url("satellite_configurations");
540
541        self.get_paginated(uri)
542    }
543
544    /// Produces a single satellite configuration matching the provided satellite configuration ID
545    fn get_satellite_configuration_by_id(
546        &self,
547        satellite_configuration_id: i32,
548    ) -> impl Future<Output = Result<Self::Container<SatelliteConfiguration>, Error>> + Send + Sync
549    {
550        async move {
551            let uri = self.path_to_url(format!(
552                "satellite_configurations/{satellite_configuration_id}"
553            ));
554
555            self.get_json_map(uri).await
556        }
557    }
558
559    /// Produces a single satellite configuration matching the provided satellite configuration name
560    fn get_satellite_configuration_by_name(
561        &self,
562        satellite_configuration_name: &str,
563    ) -> impl Future<Output = Result<Self::Container<SatelliteConfiguration>, Error>> + Send + Sync
564    {
565        async move {
566            let mut uri = self.path_to_url("satellite_configurations/search/findOneByName");
567            uri.set_query(Some(&format!("name={satellite_configuration_name}")));
568
569            self.get_json_map(uri).await
570        }
571    }
572
573    /// Produces a paginated stream of [`Site`] objects.
574    ///
575    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
576    /// and return type
577    fn get_sites(&self) -> PaginatedStream<'_, Self::Container<Site>> {
578        let uri = self.path_to_url("sites");
579        self.get_paginated(uri)
580    }
581
582    /// Produces a single [`Site`] object matching the provided ID.
583    ///
584    /// See [`get`](Self::get) documentation for more details about the process and return type
585    fn get_site_by_id(
586        &self,
587        id: i32,
588    ) -> impl Future<Output = Result<Self::Container<Site>, Error>> + Send + Sync {
589        async move {
590            let uri = self.path_to_url(format!("sites/{id}"));
591            self.get_json_map(uri).await
592        }
593    }
594
595    /// Produces a single [`Site`] object matching the provided name.
596    ///
597    /// See [`get`](Self::get) documentation for more details about the process and return type
598    fn get_site_by_name(
599        &self,
600        name: impl AsRef<str> + Send + Sync,
601    ) -> impl Future<Output = Result<Self::Container<Site>, Error>> + Send + Sync {
602        async move {
603            let mut uri = self.path_to_url("sites/search/findOneByName");
604            let query = format!("name={}", name.as_ref());
605            uri.set_query(Some(&query));
606
607            self.get_json_map(uri).await
608        }
609    }
610
611    /// Produces a single [`TaskRequest`] matching the provided ID.
612    ///
613    /// See [`get`](Self::get) documentation for more details about the process and return type
614    fn get_request_by_id(
615        &self,
616        task_request_id: i32,
617    ) -> impl Future<Output = Result<Self::Container<TaskRequest>, Error>> + Send + Sync {
618        async move {
619            let uri = self.path_to_url(format!("requests/{task_request_id}"));
620
621            self.get_json_map(uri).await
622        }
623    }
624
625    /// Produces a paginated stream of [`TaskRequest`] objects.
626    ///
627    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
628    /// and return type
629    fn get_requests(&self) -> PaginatedStream<'_, Self::Container<TaskRequest>> {
630        {
631            let uri = self.path_to_url("requests/search/findAll");
632            self.get_paginated(uri)
633        }
634    }
635
636    /// Produces a vector of [`TaskRequest`] items, representing all the task requests matching the
637    /// target time overlapping with the provided time range.
638    fn get_requests_by_target_date_between(
639        &self,
640        start: OffsetDateTime,
641        end: OffsetDateTime,
642    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync {
643        async move {
644            let mut uri = self.path_to_url("requests/search/findAllByTargetDateBetween");
645
646            uri.set_query(Some(&format!(
647                "start={}&end={}",
648                start.format(&Iso8601::DEFAULT).unwrap(),
649                end.format(&Iso8601::DEFAULT).unwrap(),
650            )));
651
652            self.get_json_map(uri).await
653        }
654    }
655
656    /// Produces a vector of [`TaskRequest`] items,
657    /// representing all the task requests matching the account at the provided URI and whose
658    /// target time overlaps with the provided time range.
659    ///
660    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
661    /// and return type
662    fn get_requests_by_account_and_target_date_between<T>(
663        &self,
664        account_uri: T,
665        start: OffsetDateTime,
666        end: OffsetDateTime,
667    ) -> PaginatedStream<'_, Self::Container<TaskRequest>>
668    where
669        T: AsRef<str> + Send + Sync,
670    {
671        let mut uri = self.path_to_url("requests/search/findAllByAccountAndTargetDateBetween");
672
673        uri.set_query(Some(&format!(
674            "account={}&start={}&end={}",
675            account_uri.as_ref(),
676            start.format(&Iso8601::DEFAULT).unwrap(),
677            end.format(&Iso8601::DEFAULT).unwrap(),
678        )));
679
680        self.get_paginated(uri)
681    }
682
683    /// Produces a paginated stream of [`TaskRequest`]
684    /// objects whose account name matches the provided name, and whose pass will occur today.
685    ///
686    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
687    /// and return type
688    fn get_requests_by_account_and_upcoming_today(
689        &self,
690    ) -> PaginatedStream<'_, Self::Container<TaskRequest>> {
691        let uri = self.path_to_url("requests/search/findByAccountUpcomingToday");
692
693        self.get_paginated(uri)
694    }
695
696    /// Produces a paginated stream of [`TaskRequest`]
697    /// objects whose satellite configuration matches that of the configuration at the
698    /// `configuration_uri` endpoint.
699    ///
700    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
701    /// and return type
702    ///
703    /// # Note
704    /// The results are ordered by the creation time of the task request
705    fn get_requests_by_configuration<T>(
706        &self,
707        configuration_uri: T,
708    ) -> PaginatedStream<'_, Self::Container<TaskRequest>>
709    where
710        T: AsRef<str> + Send + Sync,
711    {
712        let mut uri = self.path_to_url("requests/search/findAllByConfigurationOrderByCreatedAsc");
713
714        uri.set_query(Some(&format!(
715            "configuration={}",
716            configuration_uri.as_ref()
717        )));
718
719        self.get_paginated::<TaskRequest>(uri)
720    }
721
722    /// Produces a vector of [`TaskRequest`] items, representing all the task requests which match
723    /// the provided configuration, whose satellite name matches one of the names provided as part
724    /// of `satellite_name`, and which overlaps the provided time range.
725    ///
726    /// See [`get`](Self::get) documentation for more details about the process and return type
727    fn get_requests_by_configuration_and_satellite_names_and_target_date_between<T, I, S>(
728        &self,
729        configuration_uri: T,
730        satellites: I,
731        start: OffsetDateTime,
732        end: OffsetDateTime,
733    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync
734    where
735        T: AsRef<str> + Send + Sync,
736        I: IntoIterator<Item = S> + Send + Sync,
737        S: AsRef<str> + Send + Sync,
738    {
739        async move {
740            let satellites_string = crate::utils::list_to_string(satellites);
741            let mut uri = self.path_to_url(
742                "requests/search/findAllByConfigurationAndSatelliteNamesAndTargetDateBetween",
743            );
744
745            uri.set_query(Some(&format!(
746                "configuration={}&satelliteNames={}&start={}&end={}",
747                configuration_uri.as_ref(),
748                satellites_string,
749                start.format(&Iso8601::DEFAULT)?,
750                end.format(&Iso8601::DEFAULT)?,
751            )));
752
753            Ok(self
754                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
755                .await?
756                .items)
757        }
758    }
759
760    /// Produces a vector of [`TaskRequest`] items, representing all the task requests matching the
761    /// configuration at the provided URI and whose target time overlaps with the provided time
762    /// range.
763    ///
764    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
765    /// and return type
766    fn get_requests_by_configuration_and_target_date_between<T>(
767        &self,
768        configuration_uri: T,
769        start: OffsetDateTime,
770        end: OffsetDateTime,
771    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync
772    where
773        T: AsRef<str> + Send + Sync,
774    {
775        async move {
776            let mut uri =
777                self.path_to_url("requests/search/findAllByConfigurationAndTargetDateBetween");
778            uri.set_query(Some(&format!(
779                "configuration={}&start={}&end={}",
780                configuration_uri.as_ref(),
781                start.format(&Iso8601::DEFAULT)?,
782                end.format(&Iso8601::DEFAULT)?,
783            )));
784
785            Ok(self
786                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
787                .await?
788                .items)
789        }
790    }
791
792    /// Produces a vector of [`TaskRequest`] items,
793    /// representing all the task requests whose ID matches one of the IDs provided as part of
794    /// `ids`.
795    ///
796    /// See [`get`](Self::get) documentation for more details about the process and return type
797    fn get_requests_by_ids<I, S>(
798        &self,
799        ids: I,
800    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync
801    where
802        I: IntoIterator<Item = S> + Send + Sync,
803        S: AsRef<str> + Send + Sync,
804    {
805        async move {
806            let ids_string = crate::utils::list_to_string(ids);
807            let mut uri = self.path_to_url("requests/search/findAllByIds");
808
809            uri.set_query(Some(&format!("ids={}", ids_string)));
810
811            Ok(self
812                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
813                .await?
814                .items)
815        }
816    }
817
818    /// Produces a paginated stream of [`TaskRequest`] objects which are public, and which overlap
819    /// with the provided time range.
820    ///
821    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
822    /// and return type
823    fn get_requests_by_overlapping_public(
824        &self,
825        start: OffsetDateTime,
826        end: OffsetDateTime,
827    ) -> PaginatedStream<'_, Self::Container<TaskRequest>> {
828        let mut uri = self.path_to_url("requests/search/findAllByOverlappingPublic");
829
830        uri.set_query(Some(&format!(
831            "start={}&end={}",
832            start.format(&Iso8601::DEFAULT).unwrap(),
833            end.format(&Iso8601::DEFAULT).unwrap(),
834        )));
835
836        self.get_paginated(uri)
837    }
838
839    /// Produces a paginated stream of [`TaskRequest`] objects whose satellite name matches one of
840    /// the names provided as part of `satellite_name`.
841    ///
842    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
843    /// and return type
844    fn get_requests_by_satellite_name<T>(
845        &self,
846        satellite_name: T,
847    ) -> PaginatedStream<'_, Self::Container<TaskRequest>>
848    where
849        T: AsRef<str> + Send + Sync,
850    {
851        let mut uri = self.path_to_url("requests/search/findBySatelliteName");
852
853        uri.set_query(Some(&format!("name={}", satellite_name.as_ref())));
854
855        self.get_paginated(uri)
856    }
857
858    /// Produces a vector of [`TaskRequest`] items, representing all the task requests whose
859    /// satellite name matches the provided name and whose target time overlaps with the provided
860    /// time range.
861    ///
862    /// See [`get`](Self::get) documentation for more details about the process and return type
863    fn get_requests_by_satellite_name_and_target_date_between<T>(
864        &self,
865        satellite_name: T,
866        start: OffsetDateTime,
867        end: OffsetDateTime,
868    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync
869    where
870        T: AsRef<str> + Send + Sync,
871    {
872        async move {
873            let mut uri =
874                self.path_to_url("requests/search/findAllBySatelliteNameAndTargetDateBetween");
875
876            uri.set_query(Some(&format!(
877                "name={}&start={}&end={}",
878                satellite_name.as_ref(),
879                start.format(&Iso8601::DEFAULT)?,
880                end.format(&Iso8601::DEFAULT)?
881            )));
882
883            Ok(self
884                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
885                .await?
886                .items)
887        }
888    }
889
890    /// Produces a paginated stream of [`TaskRequest`] objects whose status matches the provided
891    /// status.
892    ///
893    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
894    /// and return type
895    fn get_requests_by_status<T>(
896        &self,
897        status: T,
898    ) -> Result<PaginatedStream<'_, Self::Container<TaskRequest>>, Error>
899    where
900        T: TryInto<TaskStatusType> + Send + Sync,
901        Error: From<<T as TryInto<TaskStatusType>>::Error>,
902    {
903        let status: TaskStatusType = status.try_into()?;
904        let mut uri = self.path_to_url("requests/search/findByStatus");
905
906        uri.set_query(Some(&format!("status={}", status.as_ref())));
907
908        Ok(self.get_paginated(uri))
909    }
910
911    /// Produces a paginated stream of [`TaskRequest`], representing all the task requests which
912    /// match the provided status, account, and overlap the provided time range.
913    ///
914    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
915    /// and return type
916    fn get_requests_by_status_and_account_and_target_date_between<T, U>(
917        &self,
918        status: T,
919        account_uri: U,
920        start: OffsetDateTime,
921        end: OffsetDateTime,
922    ) -> PaginatedStream<'_, Self::Container<TaskRequest>>
923    where
924        T: AsRef<str> + Send + Sync,
925        U: AsRef<str> + Send + Sync,
926    {
927        let mut uri =
928            self.path_to_url("requests/search/findAllByStatusAndAccountAndTargetDateBetween");
929
930        uri.set_query(Some(&format!(
931            "status={}&satelliteNames={}&start={}&end={}",
932            status.as_ref(),
933            account_uri.as_ref(),
934            start.format(&Iso8601::DEFAULT).unwrap(),
935            end.format(&Iso8601::DEFAULT).unwrap()
936        )));
937
938        self.get_paginated(uri)
939    }
940
941    /// Produces a vector of [`TaskRequest`] items, representing all the tasks which match the
942    /// provided type, overlap with the provided time range.
943    ///
944    /// See [`get`](Self::get) documentation for more details about the process and return type
945    fn get_requests_by_type_and_target_date_between<T>(
946        &self,
947        typ: T,
948        start: OffsetDateTime,
949        end: OffsetDateTime,
950    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync
951    where
952        T: TryInto<TaskType> + Send + Sync,
953        Error: From<<T as TryInto<TaskType>>::Error>,
954    {
955        async move {
956            let typ: TaskType = typ.try_into()?;
957            let mut uri = self.path_to_url("requests/search/findAllByTypeAndTargetDateBetween");
958
959            uri.set_query(Some(&format!(
960                "type={}&start={}&end={}",
961                typ.as_ref(),
962                start.format(&Iso8601::DEFAULT)?,
963                end.format(&Iso8601::DEFAULT)?
964            )));
965
966            Ok(self
967                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
968                .await?
969                .items)
970        }
971    }
972
973    /// Produces a vector of [`TaskRequest`] items, representing the list of tasks which have
974    /// already occurred today.
975    ///
976    /// See [`get`](Self::get) documentation for more details about the process and return type
977    fn get_requests_passed_today(
978        &self,
979    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync {
980        async move {
981            let uri = self.path_to_url("requests/search/findAllPassedToday");
982
983            Ok(self
984                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
985                .await?
986                .items)
987        }
988    }
989
990    /// Produces a vector of [`TaskRequest`] items, representing the list of tasks which will occur
991    /// later today.
992    ///
993    /// See [`get`](Self::get) documentation for more details about the process and return type
994    fn get_requests_upcoming_today(
995        &self,
996    ) -> impl Future<Output = Result<Self::Container<Vec<TaskRequest>>, Error>> + Send + Sync {
997        async move {
998            let uri = self.path_to_url("requests/search/findAllUpcomingToday");
999
1000            Ok(self
1001                .get_json_map::<Embedded<Self::Container<Vec<TaskRequest>>>>(uri)
1002                .await?
1003                .items)
1004        }
1005    }
1006
1007    /// Produces a paginated stream of [`Satellite`] objects.
1008    ///
1009    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
1010    /// and return type
1011    fn get_satellites(&self) -> PaginatedStream<'_, Self::Container<Satellite>> {
1012        let uri = self.path_to_url("satellites");
1013
1014        self.get_paginated(uri)
1015    }
1016
1017    /// Produces single satellite object matching the provided satellite ID
1018    fn get_satellite_by_id(
1019        &self,
1020        satellite_id: i32,
1021    ) -> impl Future<Output = Result<Self::Container<Satellite>, Error>> + Send + Sync {
1022        async move {
1023            let uri = self.path_to_url(format!("satellites/{}", satellite_id));
1024
1025            self.get_json_map(uri).await
1026        }
1027    }
1028
1029    /// Produces single satellite object matching the provided satellite name
1030    fn get_satellite_by_name(
1031        &self,
1032        satellite_name: &str,
1033    ) -> impl Future<Output = Result<Self::Container<Satellite>, Error>> + Send + Sync {
1034        async move {
1035            let mut uri = self.path_to_url("satellites/findOneByName");
1036            uri.set_query(Some(&format!("name={satellite_name}")));
1037
1038            self.get_json_map(uri).await
1039        }
1040    }
1041
1042    /// Produces a single [`Task`] matching the provided ID.
1043    ///
1044    /// See [`get`](Self::get) documentation for more details about the process and return type
1045    fn get_task_by_id(
1046        &self,
1047        task_id: i32,
1048    ) -> impl Future<Output = Result<Self::Container<Task>, Error>> + Send + Sync {
1049        async move {
1050            let uri = self.path_to_url(format!("tasks/{}", task_id));
1051
1052            self.get_json_map(uri).await
1053        }
1054    }
1055
1056    /// Produces a vector of [`Task`] items, representing all the tasks which match the provided
1057    /// account, and intersect with the provided time frame.
1058    ///
1059    /// See [`get`](Self::get) documentation for more details about the process and return type
1060    fn get_tasks_by_account_and_pass_overlapping<T>(
1061        &self,
1062        account_uri: T,
1063        start: OffsetDateTime,
1064        end: OffsetDateTime,
1065    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync
1066    where
1067        T: AsRef<str> + Send + Sync,
1068    {
1069        async move {
1070            let mut uri = self.path_to_url("tasks/search/findByAccountAndPassOverlapping");
1071
1072            uri.set_query(Some(&format!(
1073                "account={}&start={}&end={}",
1074                account_uri.as_ref(),
1075                start.format(&Iso8601::DEFAULT)?,
1076                end.format(&Iso8601::DEFAULT)?
1077            )));
1078
1079            Ok(self
1080                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1081                .await?
1082                .items)
1083        }
1084    }
1085
1086    /// Produces a vector of [`Task`] items, representing all the tasks which match the provided
1087    /// account, satellite, band, and intersect with the provided time frame.
1088    ///
1089    /// See [`get`](Self::get) documentation for more details about the process and return type
1090    fn get_tasks_by_account_and_satellite_and_band_and_pass_overlapping<T, U, V>(
1091        &self,
1092        account_uri: T,
1093        satellite_config_uri: U,
1094        band: V,
1095        start: OffsetDateTime,
1096        end: OffsetDateTime,
1097    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync
1098    where
1099        T: AsRef<str> + Send + Sync,
1100        U: AsRef<str> + Send + Sync,
1101        V: AsRef<str> + Send + Sync,
1102    {
1103        async move {
1104            let mut uri = self.path_to_url(
1105                "tasks/search/findByAccountAndSiteConfigurationAndBandAndPassOverlapping",
1106            );
1107
1108            uri.set_query(Some(&format!(
1109                "account={}&satellite={}&band={}&start={}&end={}",
1110                account_uri.as_ref(),
1111                satellite_config_uri.as_ref(),
1112                band.as_ref(),
1113                start.format(&Iso8601::DEFAULT)?,
1114                end.format(&Iso8601::DEFAULT)?,
1115            )));
1116
1117            Ok(self
1118                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1119                .await?
1120                .items)
1121        }
1122    }
1123
1124    /// Produces a vector of [`Task`] representing all the tasks which match the provided account,
1125    /// site configuration, band, and intersect with the provided time frame.
1126    ///
1127    /// See [`get`](Self::get) documentation for more details about the process and return type
1128    fn get_tasks_by_account_and_site_configuration_and_band_and_pass_overlapping<T, U, V>(
1129        &self,
1130        account_uri: T,
1131        site_config_uri: U,
1132        band: V,
1133        start: OffsetDateTime,
1134        end: OffsetDateTime,
1135    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync
1136    where
1137        T: AsRef<str> + Send + Sync,
1138        U: AsRef<str> + Send + Sync,
1139        V: AsRef<str> + Send + Sync,
1140    {
1141        async move {
1142            let mut uri = self.path_to_url(
1143                "tasks/search/findByAccountAndSiteConfigurationAndBandAndPassOverlapping",
1144            );
1145
1146            uri.set_query(Some(&format!(
1147                "account={}&siteConfig={}&band={}&start={}&end={}",
1148                account_uri.as_ref(),
1149                site_config_uri.as_ref(),
1150                band.as_ref(),
1151                start.format(&Iso8601::DEFAULT)?,
1152                end.format(&Iso8601::DEFAULT)?
1153            )));
1154
1155            Ok(self
1156                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1157                .await?
1158                .items)
1159        }
1160    }
1161
1162    /// Produces a vector of [`Task`] items, representing all the tasks contained within the
1163    /// provided time frame.
1164    ///
1165    /// See [`get`](Self::get) documentation for more details about the process and return type
1166    ///
1167    /// # Note
1168    ///
1169    /// This differs from [`Self::get_tasks_by_pass_overlapping`] in that it only produces tasks
1170    /// which are wholly contained within the window.
1171    fn get_tasks_by_pass_window(
1172        &self,
1173        start: OffsetDateTime,
1174        end: OffsetDateTime,
1175    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync {
1176        async move {
1177            let mut uri = self.path_to_url("tasks/search/findByStartBetweenOrderByStartAsc");
1178
1179            uri.set_query(Some(&format!(
1180                "start={}&end={}",
1181                start.format(&Iso8601::DEFAULT)?,
1182                end.format(&Iso8601::DEFAULT)?
1183            )));
1184
1185            Ok(self
1186                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1187                .await?
1188                .items)
1189        }
1190    }
1191
1192    /// Produces a paginated stream of [`Task`] items, representing all the tasks which overlap the
1193    /// provided time frame.
1194    ///
1195    /// See [`get`](Self::get) documentation for more details about the process and return type
1196    ///
1197    /// # Note
1198    ///
1199    /// This differs from [`Self::get_tasks_by_pass_window`] in that it also includes tasks which
1200    /// only partially fall within the provided time frame.
1201    fn get_tasks_by_pass_overlapping(
1202        &self,
1203        start: OffsetDateTime,
1204        end: OffsetDateTime,
1205    ) -> PaginatedStream<'_, Self::Container<Task>> {
1206        let start = match start.format(&Iso8601::DEFAULT).map_err(Error::from) {
1207            Ok(start) => start,
1208            Err(error) => return error.once_err(),
1209        };
1210
1211        let end = match end.format(&Iso8601::DEFAULT).map_err(Error::from) {
1212            Ok(end) => end,
1213            Err(error) => return error.once_err(),
1214        };
1215
1216        let mut uri = self.path_to_url("tasks/search/findByOverlapping");
1217
1218        uri.set_query(Some(&format!("start={}&end={}", start, end)));
1219
1220        self.get_paginated(uri)
1221    }
1222
1223    /// Produces a vector of [`Task`] items, representing the list of tasks which have already
1224    /// occurred today.
1225    ///
1226    /// See [`get`](Self::get) documentation for more details about the process and return type
1227    fn get_tasks_passed_today(
1228        &self,
1229    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync {
1230        async move {
1231            let uri = self.path_to_url("tasks/search/findAllPassedToday");
1232
1233            Ok(self
1234                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1235                .await?
1236                .items)
1237        }
1238    }
1239
1240    /// Produces a vector of [`Task`] items, representing the list of tasks which will occur later
1241    /// today.
1242    ///
1243    /// See [`get`](Self::get) documentation for more details about the process and return type
1244    fn get_tasks_upcoming_today(
1245        &self,
1246    ) -> impl Future<Output = Result<Self::Container<Vec<Task>>, Error>> + Send + Sync {
1247        async move {
1248            let uri = self.path_to_url("tasks/search/findAllUpcomingToday");
1249
1250            Ok(self
1251                .get_json_map::<Embedded<Self::Container<Vec<Task>>>>(uri)
1252                .await?
1253                .items)
1254        }
1255    }
1256
1257    /// Produces a paginated stream of [`User`] objects.
1258    ///
1259    /// See [`get_paginated`](Self::get_paginated) documentation for more details about the process
1260    /// and return type
1261    fn get_users(&self) -> PaginatedStream<'_, Self::Container<User>> {
1262        let uri = self.path_to_url("users");
1263        self.get_paginated(uri)
1264    }
1265
1266    /// Create a new satellite band object
1267    ///
1268    /// # Example
1269    ///
1270    /// ```no_run
1271    /// # use freedom_api::prelude::*;
1272    /// # tokio_test::block_on(async {
1273    /// let client = Client::from_env()?;
1274    ///
1275    /// client
1276    ///     .new_band_details()
1277    ///     .name("My Satellite Band")
1278    ///     .band_type(BandType::Receive)
1279    ///     .frequency(8096.0)
1280    ///     .default_band_width(1.45)
1281    ///     .io_hardware(IoHardware::Modem)
1282    ///     .send()
1283    ///     .await?;
1284    /// # Ok::<_, Box<dyn std::error::Error>>(())
1285    /// # });
1286    /// ```
1287    fn new_band_details(&self) -> post::band::BandDetailsBuilder<'_, Self, post::band::NoName>
1288    where
1289        Self: Sized,
1290    {
1291        post::band::new(self)
1292    }
1293
1294    /// Create a new satellite configuration
1295    ///
1296    /// # Example
1297    ///
1298    /// ```no_run
1299    /// # use freedom_api::prelude::*;
1300    /// # tokio_test::block_on(async {
1301    /// let client = Client::from_env()?;
1302    ///
1303    /// client
1304    ///     .new_satellite_configuration()
1305    ///     .name("My Satellite Configuration")
1306    ///     .band_ids([1, 2, 3]) // List of band IDs to associate with config
1307    ///     .send()
1308    ///     .await?;
1309    /// # Ok::<_, Box<dyn std::error::Error>>(())
1310    /// # });
1311    /// ```
1312    fn new_satellite_configuration(
1313        &self,
1314    ) -> post::sat_config::SatelliteConfigurationBuilder<'_, Self, post::sat_config::NoName>
1315    where
1316        Self: Sized,
1317    {
1318        post::sat_config::new(self)
1319    }
1320
1321    /// Create a new satellite
1322    ///
1323    /// # Example
1324    ///
1325    /// ```no_run
1326    /// # use freedom_api::prelude::*;
1327    /// # tokio_test::block_on(async {
1328    /// let client = Client::from_env()?;
1329    ///
1330    /// client
1331    ///     .new_satellite()
1332    ///     .name("My Satellite")
1333    ///     .satellite_configuration_id(42)
1334    ///     .norad_id(3600)
1335    ///     .description("A test satellite")
1336    ///     .send()
1337    ///     .await?;
1338    /// # Ok::<_, Box<dyn std::error::Error>>(())
1339    /// # });
1340    /// ```
1341    fn new_satellite(&self) -> post::satellite::SatelliteBuilder<'_, Self, post::satellite::NoName>
1342    where
1343        Self: Sized,
1344    {
1345        post::satellite::new(self)
1346    }
1347
1348    /// Create a new override
1349    ///
1350    /// # Example
1351    ///
1352    /// ```no_run
1353    /// # use freedom_api::prelude::*;
1354    /// # tokio_test::block_on(async {
1355    /// let client = Client::from_env()?;
1356    ///
1357    /// client
1358    ///     .new_override()
1359    ///     .name("downconverter.gain override for sat 1 on config 2")
1360    ///     .satellite_id(1)
1361    ///     .satellite_configuration_id(2)
1362    ///     .add_property("site.hardware.modem.ttc.rx.demodulator.bitrate", 8096_u32)
1363    ///     .add_property("site.hardware.modem.ttc.tx.modulator.bitrate", 8096_u32)
1364    ///     .send()
1365    ///     .await?;
1366    /// # Ok::<_, Box<dyn std::error::Error>>(())
1367    /// # });
1368    /// ```
1369    fn new_override(&self) -> post::overrides::OverrideBuilder<'_, Self, post::overrides::NoName>
1370    where
1371        Self: Sized,
1372    {
1373        post::overrides::new(self)
1374    }
1375
1376    /// Create a new user
1377    ///
1378    /// # Example
1379    ///
1380    /// ```no_run
1381    /// # use freedom_api::prelude::*;
1382    /// # tokio_test::block_on(async {
1383    /// let client = Client::from_env()?;
1384    ///
1385    /// client
1386    ///     .new_user()
1387    ///     .account_id(1)
1388    ///     .first_name("Han")
1389    ///     .last_name("Solo")
1390    ///     .email("flyingsolo@gmail.com")
1391    ///     .send()
1392    ///     .await?;
1393    /// # Ok::<_, Box<dyn std::error::Error>>(())
1394    /// # });
1395    /// ```
1396    fn new_user(&self) -> post::user::UserBuilder<'_, Self, post::user::NoAccount>
1397    where
1398        Self: Sized,
1399    {
1400        post::user::new(self)
1401    }
1402
1403    /// Create a new task request
1404    ///
1405    /// # Example
1406    ///
1407    /// ```no_run
1408    /// # use freedom_api::prelude::*;
1409    /// # use time::OffsetDateTime;
1410    /// # use std::time::Duration;
1411    /// # tokio_test::block_on(async {
1412    /// let client = Client::from_env()?;
1413    ///
1414    /// client
1415    ///     .new_task_request()
1416    ///     .test_task("my_test_file.bin")
1417    ///     .target_time_utc(OffsetDateTime::now_utc() + Duration::from_secs(15 * 60))
1418    ///     .task_duration(120)
1419    ///     .satellite_id(1016)
1420    ///     .site_id(27)
1421    ///     .site_configuration_id(47)
1422    ///     .band_ids([2017, 2019])
1423    ///     .send()
1424    ///     .await?;
1425    /// # Ok::<_, Box<dyn std::error::Error>>(())
1426    /// # });
1427    /// ```
1428    fn new_task_request(&self) -> post::TaskRequestBuilder<'_, Self, post::request::NoType>
1429    where
1430        Self: Sized,
1431    {
1432        post::request::new(self)
1433    }
1434
1435    /// Fetch an FPS token for the provided band ID and site configuration ID
1436    ///
1437    /// # Example
1438    ///
1439    /// ```no_run
1440    /// # use freedom_api::prelude::*;
1441    /// # tokio_test::block_on(async {
1442    /// const BAND_ID: u32 = 42;
1443    /// const SITE_CONFIG_ID: u32 = 201;
1444    ///
1445    /// let client = Client::from_env()?;
1446    ///
1447    /// let token = client.new_token_by_site_configuration_id(BAND_ID, SITE_CONFIG_ID).await?;
1448    /// // Submit token to FPS ...
1449    /// println!("{:?}", token);
1450    /// # Ok::<_, Box<dyn std::error::Error>>(())
1451    /// # });
1452    /// ```
1453    fn new_token_by_site_configuration_id(
1454        &self,
1455        band_id: u32,
1456        site_configuration_id: u32,
1457    ) -> impl Future<Output = Result<String, Error>> + Send + Sync {
1458        async move {
1459            let url = self.path_to_url("fps");
1460            let payload = serde_json::json!({
1461                "band": format!("/api/satellite_bands/{}", band_id),
1462                "configuration": format!("/api/configurations/{}", site_configuration_id),
1463            });
1464
1465            let value: JsonValue = self.post_deserialize(url, &payload).await?;
1466
1467            value
1468                .get("token")
1469                .ok_or(Error::Response(String::from("Missing token field")))?
1470                .as_str()
1471                .ok_or(Error::Response(String::from("Invalid type for token")))
1472                .map(|s| s.to_owned())
1473        }
1474    }
1475
1476    /// Fetch an FPS token for the provided band ID and satellite ID
1477    ///
1478    /// # Example
1479    ///
1480    /// ```no_run
1481    /// # use freedom_api::prelude::*;
1482    /// # tokio_test::block_on(async {
1483    /// const BAND_ID: u32 = 42;
1484    /// const SATELLITE_ID: u32 = 101;
1485    ///
1486    /// let client = Client::from_env()?;
1487    ///
1488    /// let token = client.new_token_by_satellite_id(BAND_ID, SATELLITE_ID).await?;
1489    /// // Submit token to FPS ...
1490    /// println!("{:?}", token);
1491    /// # Ok::<_, Box<dyn std::error::Error>>(())
1492    /// # });
1493    /// ```
1494    fn new_token_by_satellite_id(
1495        &self,
1496        band_id: u32,
1497        satellite_id: u32,
1498    ) -> impl Future<Output = Result<String, Error>> + Send + Sync {
1499        async move {
1500            let url = self.path_to_url("fps");
1501            let payload = serde_json::json!({
1502                "band": format!("/api/satellite_bands/{}", band_id),
1503                "satellite": format!("/api/satellites/{}", satellite_id),
1504            });
1505
1506            let value: JsonValue = self.post_deserialize(url, &payload).await?;
1507
1508            value
1509                .get("token")
1510                .ok_or(Error::Response(String::from("Missing token field")))?
1511                .as_str()
1512                .ok_or(Error::Response(String::from("Invalid type for token")))
1513                .map(|s| s.to_owned())
1514        }
1515    }
1516}
1517
1518fn error_on_non_success(status: &StatusCode) -> Result<(), Error> {
1519    if !status.is_success() {
1520        return Err(Error::Response(status.to_string()));
1521    }
1522
1523    Ok(())
1524}