fav_core 0.0.1-alpha5

Fav's core crate; A collection of traits.
Documentation
//! The `Operations` trait,
//! making app able to perform more operations

use crate::{
    api::ApiProvider, config::Config, error::FavCoreError, meta::Meta, res::ResSet, FavCoreResult,
};
use core::future::Future;
use reqwest::{Client, Response};

/// Making a client able to perform operations.
///
/// Work with [`ApiProvider`] and [`Config`] to perform operations.
/// The operations supported should be included in enum `K`.
/// - [`LocalOperations`]'s async methods cannot be Send.
/// - [`Operations`] is generated by [`trait_variant::make`], which implements `Send`.
/// For more information, see [Rust Blog](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html#async-fn-in-public-traits).
/// # Example
/// ```no_run
/// # #[path = "test_utils/mod.rs"]
/// # mod test_utils;
/// use test_utils::data::App;
/// use fav_core::ops::Operations;
/// # #[tokio::main]
/// # async fn main() {
///     let mut app = App::default();
///     app.login().await.unwrap();
/// # }
/// ```
/// `App` above is a struct that implements `LocalOperations`/`Operations`,
/// see [concret implementation](https://github.com/kingwingfly/fav/blob/dev/fav_core/src/test_utils/impls.rs).
#[allow(missing_docs)]
#[trait_variant::make(Operations: Send)]
pub trait LocalOperations<K>: ApiProvider<K> + Config
where
    K: Send,
{
    async fn login(&mut self) -> FavCoreResult<()>;
    async fn logout(&mut self) -> FavCoreResult<()>;
    /// Fetch one resource
    /// # Caution
    /// One needs to handle Ctrl-C with `tokio::signal::ctrl_c` and `tokio::select!`,
    /// and return [`FavCoreError::Cancel`]
    async fn fetch(&self, resource: &mut impl Meta) -> FavCoreResult<()>;
    /// Pull one resource.
    /// # Caution
    /// One needs to handle Ctrl-C with `tokio::signal::ctrl_c` and `tokio::select!`,
    /// and return [`FavCoreError::Cancel`]
    async fn pull(&self, resource: &mut impl Meta) -> FavCoreResult<()>;

    /// Return a `&'static reqwest::Client`, use it to perform operations during the lifetime of the client.
    /// # Example
    /// ```no_run
    /// use std::sync::OnceLock;
    /// use reqwest::Client;
    /// // In `Operations`'s implementation
    /// fn client() -> &'static Client {
    ///     static CLIENT: OnceLock<Client> = OnceLock::new();
    ///     CLIENT.get_or_init(Client::new)
    /// }
    /// ```
    /// In practice, one should use [`Config`] to make a `Client` that meet the demand.
    fn client(&self) -> &'static Client {
        use std::sync::OnceLock;
        let headers = self.headers();
        static CLIENT: OnceLock<Client> = OnceLock::new();
        CLIENT.get_or_init(|| Client::builder().default_headers(headers).build().unwrap())
    }

    /// Request the api, which is returned by `Api::api(api_kind)`,
    /// and with the method, which is returned `Api::method()`.
    /// Use the provided params, and client with default headers `Config::headers()`.
    fn request(
        &self,
        api_kind: K,
        params: impl IntoIterator<Item = &'static str> + Send,
    ) -> impl Future<Output = FavCoreResult<Response>> {
        async {
            let client = self.client();
            let api = self.api(api_kind);
            let resp = client
                .request(
                    api.method(),
                    api.url(api.params().iter().copied().zip(params).collect()),
                )
                .send()
                .await
                .unwrap();
            Ok(resp)
        }
    }
}

/// `LocalOperationsExt`, including methods to batch fetch and pull, however,
/// it is synchronize since methods in [`LocalOperations`] is not `Send`.
/// See [`Operations`] and [`OperationsExt`] for asynchronous version.
pub trait LocalOperationsExt<K>: LocalOperations<K>
where
    K: Send,
{
    /// **Synchronously** fetch all resources using [`LocalOperations::fetch`],
    /// since `async trait` is not Send in rust by now.
    fn fetch_all(&self, resources: &mut impl ResSet) -> impl Future<Output = FavCoreResult<()>> {
        async {
            for r in resources.iter_mut() {
                if let Err(e) = self.fetch(r).await {
                    match e {
                        FavCoreError::Cancel => break,
                        _ => println!("{e}"),
                    }
                }
            }
            Ok(())
        }
    }

    /// **Synchronously** pull all resources using [`LocalOperations::pull`],
    /// since `async trait` is not Send in rust by now.
    fn pull_all(&self, resources: &mut impl ResSet) -> impl Future<Output = FavCoreResult<()>> {
        async {
            for r in resources.iter_mut() {
                if let Err(e) = self.pull(r).await {
                    match e {
                        FavCoreError::Cancel => break,
                        _ => println!("{e}"),
                    }
                }
            }
            Ok(())
        }
    }
}

impl<T: LocalOperations<K>, K: Send> LocalOperationsExt<K> for T {}

/// `OperationsExt`, including methods to batch fetch and pull.
pub trait OperationsExt<K>: Operations<K>
where
    K: Send + 'static,
{
    /// **Asynchronously** fetch resourses using [`Operations::fetch`].
    fn fetch_all(
        &'static self,
        resources: &'static mut impl ResSet,
    ) -> impl Future<Output = FavCoreResult<()>> {
        async {
            let mut rs = resources.iter_mut();
            loop {
                let batch: Vec<_> = rs.by_ref().take(10).collect();
                if batch.is_empty() {
                    break;
                }
                let jhs: Vec<_> = batch
                    .into_iter()
                    .map(|r| tokio::spawn(self.fetch(r)))
                    .collect();
                for jh in jhs {
                    if let Err(e) = jh.await.unwrap() {
                        println!("{e}");
                    }
                }
            }
            Ok(())
        }
    }

    /// **Asynchronously** pull resourses using [`Operations::pull`].
    fn pull_all(
        &'static self,
        resources: &'static mut impl ResSet,
    ) -> impl Future<Output = FavCoreResult<()>> {
        async {
            let mut rs = resources.iter_mut();
            loop {
                let batch: Vec<_> = rs.by_ref().take(10).collect();
                if batch.is_empty() {
                    break;
                }
                let jhs: Vec<_> = batch
                    .into_iter()
                    .map(|r| tokio::spawn(self.pull(r)))
                    .collect();
                for jh in jhs {
                    if let Err(e) = jh.await.unwrap() {
                        match e {
                            FavCoreError::Cancel => {
                                println!("{e}");
                                break;
                            }
                            _ => println!("{e}"),
                        }
                    }
                }
            }
            Ok(())
        }
    }
}

impl<T: Operations<K>, K: Send + 'static> OperationsExt<K> for T {}