fav_core 0.0.1-alpha7

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,
    prelude::ResSets,
    res::{Res, ResSet},
    FavCoreResult,
};
use core::future::Future;
use protobuf::MessageFull;
use reqwest::{header, Client, Response};

/// Making a client able to perform operations.
///
/// Work with [`ApiProvider`] and [`Config`] to perform operations in `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).
/// # Hint
/// Since [`LocalOperations`] is not `Send`, one should use it in a single-threaded runtime.
/// If you need async operations in a multi-threaded runtime, use [`Operations`].
///
/// To generate `Operations` required methods, use `LocalOperations` first,
/// after your editor generating the methods, change `LocalOperations` to `Operations`.
#[allow(missing_docs)]
#[trait_variant::make(Operations: Send)]
pub trait LocalOperations<SS, S, R, K>: ApiProvider<K> + Config
where
    SS: ResSets<S, R> + MessageFull,
    S: ResSet<R>,
    R: Res,
    K: Send,
{
    async fn login(&mut self) -> FavCoreResult<()>;
    async fn logout(&mut self) -> FavCoreResult<()>;
    /// Fetch all resource sets
    async fn fetch_sets(&self) -> FavCoreResult<SS>;
    /// Fetch one resource set
    async fn fetch_set(&self, set: &mut S) -> 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 R) -> 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 R) -> 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 returned by [`ApiProvider::api`],
    /// and with the method returned by `Api::method` and cookie returned by `HttpConfig::cookie_value`.
    ///
    /// Use the provided params, and client with `HttpConfig::headers`.
    fn request(
        &self,
        api_kind: K,
        params: &[&str], // Todo make this arg more generic
    ) -> impl Future<Output = FavCoreResult<Response>> {
        async {
            let client = self.client();
            let api = self.api(api_kind);
            let cookie = self.cookie_value(api.cookie_keys());
            let resp = client
                .request(api.method(), api.url(params))
                .header(header::COOKIE, cookie)
                .send()
                .await?;
            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<SS, S, R, K>: LocalOperations<SS, S, R, K>
where
    SS: ResSets<S, R> + MessageFull,
    S: ResSet<R>,
    R: Res,
    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 S) -> 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 S) -> 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, SS, S, R, K> LocalOperationsExt<SS, S, R, K> for T
where
    T: LocalOperations<SS, S, R, K>,
    SS: ResSets<S, R> + MessageFull,
    S: ResSet<R>,
    R: Res,
    K: Send,
{
}

/// `OperationsExt`, including methods to batch fetch and pull.
pub trait OperationsExt<SS, S, R, K>: Operations<SS, S, R, K>
where
    SS: ResSets<S, R> + MessageFull + 'static,
    S: ResSet<R> + 'static,
    R: Res + 'static,
    K: Send + 'static,
{
    /// **Asynchronously** fetch resourses using [`Operations::fetch`].
    fn fetch_all(
        &'static self,
        resources: &'static mut S,
    ) -> 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 S,
    ) -> 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, SS, S, R, K> OperationsExt<SS, S, R, K> for T
where
    T: Operations<SS, S, R, K>,
    SS: ResSets<S, R> + MessageFull + 'static,
    S: ResSet<R> + 'static,
    R: Res + 'static,
    K: Send + 'static,
{
}