cinnamon 0.1.1

A type-safe Nightscout client for Rust, aiming to simplify the interactions with the confusing Nightscout API.
Documentation
use crate::error::NightscoutError;

use super::client::NightscoutClient;
use crate::endpoints::Endpoint;

use std::future::{Future, IntoFuture};
use std::marker::PhantomData;
use std::pin::Pin;

use anyhow::Result;
use chrono::{DateTime, Utc};
use futures::stream::{self, StreamExt};
use reqwest::Method;
use serde::de::DeserializeOwned;

#[derive(Clone, Debug, PartialEq)]
pub enum Device {
    Auto,
    All,
    Custom(String),
}

pub trait HasDevice {
    fn device(&self) -> Option<&str>;
}

pub struct QueryBuilder<T> {
    client: NightscoutClient,
    endpoint: Endpoint,
    from_date: Option<DateTime<Utc>>,
    to_date: Option<DateTime<Utc>>,
    count: usize,
    method: Method,
    id: Option<String>,
    device: Device,
    _marker: PhantomData<T>,
}

impl<T> QueryBuilder<T> {
    pub fn new(client: NightscoutClient, endpoint: Endpoint, method: Method) -> Self {
        Self {
            client,
            endpoint,
            from_date: None,
            to_date: None,
            count: 10,
            method,
            id: None,
            device: Device::All,
            _marker: PhantomData,
        }
    }

    pub fn from(mut self, date: DateTime<Utc>) -> Self {
        self.from_date = Some(date);
        self
    }

    pub fn to(mut self, date: DateTime<Utc>) -> Self {
        self.to_date = Some(date);
        self
    }

    pub fn limit(mut self, count: usize) -> Self {
        self.count = count;
        self
    }

    pub fn id(mut self, id: impl Into<String>) -> Self {
        self.id = Some(id.into());
        self
    }

    pub fn device(mut self, device: Device) -> Self {
        self.device = device;
        self
    }
}

impl<T> IntoFuture for QueryBuilder<T>
where
    T: DeserializeOwned + Send + Sync + 'static + HasDevice,
{
    type Output = Result<Vec<T>, NightscoutError>;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move {
            let path = if let Some(id) = &self.id {
                format!("{}/{}", self.endpoint.as_path(), id)
            } else {
                self.endpoint.as_path().to_string()
            };

            let mut url = self.client.base_url.join(&path)?;

            {
                let mut query = url.query_pairs_mut();

                if self.id.is_none() {
                    query.append_pair("count", &self.count.to_string());

                    if let Some(from) = self.from_date {
                        query.append_pair("find[dateString][$gte]", &from.to_rfc3339());
                    }

                    if let Some(to) = self.to_date {
                        query.append_pair("find[dateString][$lte]", &to.to_rfc3339());
                    }
                }
            }

            match self.method {
                Method::GET => {
                    let mut request = self.client.http.get(url);
                    request = self.client.auth(request);

                    let response = self.client.send_checked(request).await?;
                    let items = response.json::<Vec<T>>().await?;

                    match &self.device {
                        Device::All => Ok(items),
                        Device::Custom(name) => {
                            let filtered: Vec<T> = items
                                .into_iter()
                                .filter(|item| item.device() == Some(name.as_str()))
                                .collect();
                            Ok(filtered)
                        }
                        Device::Auto => {
                            let target_device = items
                                .iter()
                                .find_map(|item| item.device())
                                .map(|s| s.to_string());

                            if let Some(device_name) = target_device {
                                let filtered: Vec<T> = items
                                    .into_iter()
                                    .filter(|item| item.device() == Some(device_name.as_str()))
                                    .collect();
                                Ok(filtered)
                            } else {
                                Ok(items)
                            }
                        }
                    }
                }
                Method::DELETE => {
                    if self.id.is_some() {
                        // Single ID delete logic
                        let mut get_req = self.client.http.get(url.clone());
                        get_req = self.client.auth(get_req);
                        let item = self
                            .client
                            .send_checked(get_req)
                            .await?
                            .json::<Vec<T>>()
                            .await?;

                        let mut del_req = self.client.http.delete(url);
                        del_req = self.client.auth(del_req);
                        self.client.send_checked(del_req).await?;

                        Ok(item)
                    } else {
                        let mut get_request = self.client.http.get(url.clone());
                        get_request = self.client.auth(get_request);

                        let get_response = self.client.send_checked(get_request).await?;
                        let items: Vec<serde_json::Value> = get_response.json().await?;

                        let delete_urls: Vec<reqwest::Url> = items
                            .iter()
                            .filter_map(|item| {
                                let id = item.get("_id")?.as_str()?;
                                let delete_path = format!("{}/{}", self.endpoint.as_path(), id);
                                self.client.base_url.join(&delete_path).ok()
                            })
                            .collect();

                        let delete_tasks = delete_urls.into_iter().map(|url| {
                            let client = self.client.clone();
                            async move {
                                let mut req = client.http.delete(url);
                                req = client.auth(req);
                                client.send_checked(req).await
                            }
                        });

                        stream::iter(delete_tasks)
                            .buffer_unordered(10)
                            .collect::<Vec<_>>()
                            .await;

                        let t_items: Vec<T> =
                            serde_json::from_value(serde_json::Value::Array(items))?;
                        Ok(t_items)
                    }
                }
                _ => Err(NightscoutError::Unknown),
            }
        })
    }
}