gw2lib 3.0.2

A simple to use Gw2 API wrapper library
Documentation
use std::{
    fmt::{Display, Write},
    hash::Hash,
};

use async_trait::async_trait;
use chrono::{NaiveDateTime, Utc};
use gw2lib_model::{Endpoint, Language};
use redis::{
    aio::{Connection, ConnectionLike},
    AsyncCommands, Client, Cmd, RedisError,
};
use serde::{de::DeserializeOwned, Serialize};

use crate::cache::Cache;

#[derive(Debug, Clone)]
pub struct RedisCache {
    client: Client,
}

#[async_trait]
impl Cache for RedisCache {
    async fn insert<T, I, E, A>(
        &self,
        id: &I,
        endpoint: &T,
        expiring: NaiveDateTime,
        lang: Language,
        auth: &Option<A>,
    ) where
        T: DeserializeOwned + Serialize + Clone + Send + Sync + Sync + 'static,
        I: Display + Hash + Sync + 'static + ?Sized,
        E: Endpoint,
        A: Display + Hash + Sync + 'static,
    {
        let mut conn = match self.connection().await {
            Some(conn) => conn,
            None => return,
        };
        let ex = expiring - Utc::now().naive_utc();
        let key = self.gen_key::<E, I, A>(id, lang, auth);
        if let Ok(value) = serde_json::to_string(endpoint) {
            conn.set_ex::<_, _, ()>(key, value, ex.num_seconds().try_into().unwrap_or_default())
                .await
                .ok();
        }
    }

    async fn get<T, I, E, A>(&self, id: &I, lang: Language, auth: &Option<A>) -> Option<T>
    where
        T: DeserializeOwned + Serialize + Clone + Send + Sync + Sync + 'static,
        I: Display + Hash + Sync + 'static + ?Sized,
        E: Endpoint,
        A: Display + Hash + Sync + 'static,
    {
        let mut conn = self.connection().await?;
        let key = self.gen_key::<E, I, A>(id, lang, auth);
        conn.get(key)
            .await
            .ok()
            .and_then(|x: String| serde_json::from_str(&x).ok())
    }

    async fn cleanup(&self) {}

    async fn wipe_static(&self) {
        self.delete_keys("gw2lib_static_*").await;
    }

    async fn wipe_authenticated(&self) {
        self.delete_keys("gw2lib_auth_*").await;
    }
}

impl RedisCache {
    pub fn new(client: Client) -> Self {
        Self { client }
    }
}

impl RedisCache {
    async fn connection(&self) -> Option<Connection> {
        self.client.get_async_connection().await.ok()
    }

    async fn delete_keys(&self, pattern: &str) {
        let mut conn = match self.connection().await {
            Some(conn) => conn,
            None => return,
        };
        let cmd = redis::cmd("SCAN").arg("MATCH").arg(pattern).clone();
        let mut cursor = 0;

        loop {
            Self::delete_keys_from_cursor(&mut conn, cmd.clone(), &mut cursor)
                .await
                .ok();

            if cursor == 0 {
                break;
            }
        }
    }

    async fn delete_keys_from_cursor(
        conn: &mut Connection,
        mut cmd: Cmd,
        cursor: &mut u64,
    ) -> Result<(), RedisError> {
        let cmd = cmd.cursor_arg(*cursor);

        let res = conn.req_packed_command(cmd).await?;

        let (cur, chunk) = if res.looks_like_cursor() {
            redis::from_redis_value::<(u64, Vec<String>)>(&res)?
        } else {
            let res = redis::from_redis_value(&res)?;
            (0, res)
        };
        *cursor = cur;

        conn.del::<_, ()>(chunk).await
    }

    fn gen_key<E: Endpoint, I: Display + ?Sized, A: Display>(
        &self,
        id: &I,
        lang: Language,
        auth: &Option<A>,
    ) -> String {
        let mut key = String::with_capacity(128);
        let mut push = |s: &str| {
            key.push_str(s);
            key.push('_');
        };

        push("gw2lib");

        if E::AUTHENTICATED {
            push("auth");
        } else {
            push("static");
        }

        push(E::URL);

        if E::LOCALE {
            push(lang.as_str());
        }

        if E::AUTHENTICATED {
            write!(key, "{}_", auth.as_ref().unwrap()).unwrap();
        }

        write!(key, "{}", id).unwrap();

        key
    }
}