use std::collections::HashMap;
use std::sync::Mutex;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::types::ShopDomain;
pub type StoreFuture<'a, T> = BoxFuture<'a, Result<T>>;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ShopToken {
pub access_token: String,
pub scope: String,
pub expires_at: Option<i64>,
pub refresh_token: Option<String>,
pub refresh_token_expires_at: Option<i64>,
#[serde(default)]
pub installed_at: Option<i64>,
}
impl std::fmt::Debug for ShopToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShopToken")
.field("access_token", &"<redacted>")
.field("scope", &self.scope)
.field("expires_at", &self.expires_at)
.field(
"refresh_token",
&self.refresh_token.as_ref().map(|_| "<redacted>"),
)
.field("refresh_token_expires_at", &self.refresh_token_expires_at)
.field("installed_at", &self.installed_at)
.finish()
}
}
impl ShopToken {
#[must_use]
pub fn non_expiring(access_token: impl Into<String>, scope: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
scope: scope.into(),
expires_at: None,
refresh_token: None,
refresh_token_expires_at: None,
installed_at: None,
}
}
#[must_use]
pub fn with_refresh(
mut self,
expires_at: i64,
refresh_token: impl Into<String>,
refresh_token_expires_at: Option<i64>,
) -> Self {
self.expires_at = Some(expires_at);
self.refresh_token = Some(refresh_token.into());
self.refresh_token_expires_at = refresh_token_expires_at;
self
}
#[must_use]
pub const fn with_installed_at(mut self, installed_at: i64) -> Self {
self.installed_at = Some(installed_at);
self
}
#[must_use]
pub fn is_expired(&self, now: i64) -> bool {
self.expires_at.is_some_and(|expires_at| now >= expires_at)
}
#[must_use]
pub fn needs_refresh(&self, now: i64, margin_secs: i64) -> bool {
self.expires_at
.is_some_and(|expires_at| now.saturating_add(margin_secs) >= expires_at)
}
#[must_use]
pub fn can_refresh(&self, now: i64) -> bool {
self.refresh_token.is_some()
&& self
.refresh_token_expires_at
.is_none_or(|expires_at| now < expires_at)
}
#[must_use]
pub fn scopes(&self) -> Vec<&str> {
self.scope
.split(',')
.map(str::trim)
.filter(|scope| !scope.is_empty())
.collect()
}
}
pub trait TokenStore: Send + Sync + 'static {
fn get<'a>(&'a self, shop: &'a ShopDomain) -> StoreFuture<'a, Option<ShopToken>>;
fn put<'a>(&'a self, shop: &'a ShopDomain, token: ShopToken) -> StoreFuture<'a, ()>;
fn delete<'a>(&'a self, shop: &'a ShopDomain) -> StoreFuture<'a, ()>;
}
#[derive(Debug, Default)]
pub struct InMemoryTokenStore {
tokens: Mutex<HashMap<ShopDomain, ShopToken>>,
}
impl InMemoryTokenStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> Result<usize> {
self.lock().map(|tokens| tokens.len())
}
fn lock(&self) -> Result<std::sync::MutexGuard<'_, HashMap<ShopDomain, ShopToken>>> {
self.tokens
.lock()
.map_err(|_| Error::Store("token store lock is poisoned".to_owned()))
}
pub fn is_empty(&self) -> Result<bool> {
self.len().map(|len| len == 0)
}
}
impl TokenStore for InMemoryTokenStore {
fn get<'a>(&'a self, shop: &'a ShopDomain) -> StoreFuture<'a, Option<ShopToken>> {
let result = self.lock().map(|tokens| tokens.get(shop).cloned());
Box::pin(async move { result })
}
fn put<'a>(&'a self, shop: &'a ShopDomain, token: ShopToken) -> StoreFuture<'a, ()> {
let result = self.lock().map(|mut tokens| {
tokens.insert(shop.clone(), token);
});
Box::pin(async move { result })
}
fn delete<'a>(&'a self, shop: &'a ShopDomain) -> StoreFuture<'a, ()> {
let result = self.lock().map(|mut tokens| {
tokens.remove(shop);
});
Box::pin(async move { result })
}
}
#[cfg(test)]
mod tests;