fav_core 0.0.1-alpha9

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,
    res::{Res, ResSet, ResSets},
    FavCoreResult,
};
use core::future::Future;
use futures::StreamExt;
use protobuf::MessageFull;
use protobuf_json_mapping::{parse_from_str_with_options, ParseOptions};
use reqwest::{header, Client, Response};
use serde::de::DeserializeOwned;
use serde_json::Value;

const PARSE_OPTIONS: ParseOptions = ParseOptions {
    ignore_unknown_fields: true,
    _future_options: (),
};

/// 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).
/// # Generic
/// - `SS`: The resource sets type, which should implement [`ResSets`].
/// - `S`: The resource set type, which should implement [`ResSet`].
/// - `R`: The resource type, which should implement [`Res`].
/// - `K`: The kind of the api, which should implement `Send`.
///
/// # 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 let your editor generate `Operations` required methods signatures, use `LocalOperations` first,
/// after your editor generating the signatures, 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>,
    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, sets: &mut SS) -> FavCoreResult<()>;
    /// Fetch one resource set
    async fn fetch_set(&self, set: &mut S) -> FavCoreResult<()>;
    /// Fetch one resource
    /// # Caution
    /// One could handle Ctrl-C with `tokio::signal::ctrl_c` and `tokio::select!`,
    /// and return [`FavCoreError::Cancel`]. This error will be handled by `OperationsExt::fetch_all`.
    async fn fetch_res(&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`]. This error will be handled by `OperationsExt::pull_all`.
    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`](crate::api::Api::method)
    /// and cookie returned by [`HttpConfig::cookie_value`](crate::config::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)
        }
    }

    /// Serde json response from [`Self::request`] to json through [`resp2json`].
    /// pointer is the pointer to the json, see [RFC6901](https://tools.ietf.org/html/rfc6901).
    fn request_json<T>(
        &self,
        api_kind: K,
        params: &[&str],
        pointer: &str,
    ) -> impl Future<Output = FavCoreResult<T>>
    where
        T: DeserializeOwned,
    {
        async {
            let resp = self.request(api_kind, params).await?;
            resp2json(resp, pointer).await
        }
    }

    /// Serde json response from [`Self::request_json`] to json first,
    /// then map it to protobuf msg through [`json2proto`].
    /// pointer is the pointer to the json, see [RFC6901](https://tools.ietf.org/html/rfc6901).
    fn request_proto<T>(
        &self,
        api_kind: K,
        params: &[&str],
        pointer: &str,
    ) -> impl Future<Output = FavCoreResult<T>>
    where
        T: MessageFull,
    {
        async {
            let json = self.request_json(api_kind, params, pointer).await?;
            json2proto(&json)
        }
    }
}

/// `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>,
    S: ResSet<R>,
    R: Res,
    K: Send,
{
    /// **Asynchronously** fetch resourses in set using [`Operations::fetch_res`].
    fn fetch_all<'a>(&self, set: &'a mut S) -> impl Future<Output = FavCoreResult<()>>
    where
        R: 'a,
    {
        batch_process(set, |r| self.fetch_res(r))
    }

    /// **Asynchronously** pull resourses in set using [`Operations::pull`].
    fn pull_all<'a>(&self, set: &'a mut S) -> impl Future<Output = FavCoreResult<()>>
    where
        R: 'a,
    {
        batch_process(set, |r| self.pull(r))
    }
}

async fn batch_process<'a, R, F, T, S>(set: &'a mut S, f: F) -> FavCoreResult<()>
where
    R: Res + 'a,
    F: FnMut(&'a mut R) -> T,
    T: Future<Output = FavCoreResult<()>>,
    S: ResSet<R>,
{
    let mut stream = tokio_stream::iter(set.iter_mut())
        .map(f)
        .buffer_unordered(10);
    while let Some(r) = stream.next().await {
        if let Err(e) = r {
            match e {
                FavCoreError::Cancel => {
                    print_warn(e);
                    break;
                }
                _ => print_err(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>,
    S: ResSet<R>,
    R: Res,
    K: Send,
{
}

/// A function to print a warning message, influenced by `tracing` feature.
/// This needn't be `inline` since warning message is not so frequent.
pub fn print_warn<T>(e: T)
where
    T: std::fmt::Display,
{
    #[cfg(not(feature = "tracing"))]
    println!("{}", e);
    #[cfg(feature = "tracing")]
    tracing::warn!("{}", e);
}

/// A function to print a err message, influenced by `tracing` feature.
/// This needn't be `inline` since error message is not so frequent.
pub fn print_err<E>(e: E)
where
    E: std::error::Error,
{
    #[cfg(not(feature = "tracing"))]
    println!("{}", e);
    #[cfg(feature = "tracing")]
    tracing::error!("{}", e);
}

/// Serde `Response` to json.
pub async fn resp2json<T>(resp: Response, pointer: &str) -> FavCoreResult<T>
where
    T: DeserializeOwned,
{
    match resp.json::<Value>().await?.pointer_mut(pointer) {
        Some(json) => {
            let ret = serde_json::from_value(json.clone())?;
            Ok(ret)
        }
        None => Err(FavCoreError::SerdePointerNotFound),
    }
}

/// Map json to proto message.
pub fn json2proto<T>(json: &Value) -> FavCoreResult<T>
where
    T: MessageFull,
{
    let json = json.to_string();
    Ok(parse_from_str_with_options(&json, &PARSE_OPTIONS)?)
}

/// Map `Response` to proto message.
pub async fn resp2proto<T>(resp: Response, pointer: &str) -> FavCoreResult<T>
where
    T: MessageFull,
{
    let json = resp2json::<Value>(resp, pointer).await?;
    json2proto(&json)
}