mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 02:21:23 +00:00
usage tracker wip
This commit is contained in:
1 parent
f17a9a034f
commit
e5e93364ae
14 files changed
+365
-14
No files matched your search
Generated
+2
@@ -5563,8 +5563,10 @@ name = "redis_schema"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"enums",
|
||||
"errors",
|
||||
"my-workspace-hack",
|
||||
"r2d2_redis",
|
||||
"tokens",
|
||||
]
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod premium_product;
|
||||
pub mod style_transfer;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod premium_product_name;
|
||||
@@ -0,0 +1,134 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[cfg(test)]
|
||||
use strum::EnumCount;
|
||||
#[cfg(test)]
|
||||
use strum::EnumIter;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// This enum is not backed by a particular database table.
|
||||
/// This is used to count premium product uses for free and paid users.
|
||||
///
|
||||
/// DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY.
|
||||
#[cfg_attr(test, derive(EnumIter, EnumCount))]
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PremiumProductName {
|
||||
// NB: These keys are kept short to preserve space
|
||||
#[serde(rename = "fa")]
|
||||
FaceAnimator,
|
||||
// NB: These keys are kept short to preserve space
|
||||
#[serde(rename = "fm")]
|
||||
FaceMirror,
|
||||
// NB: These keys are kept short to preserve space
|
||||
#[serde(rename = "lip")]
|
||||
Lipsync,
|
||||
// NB: These keys are kept short to preserve space
|
||||
#[serde(rename = "vst")]
|
||||
VideoStyleTransfer,
|
||||
}
|
||||
|
||||
// TODO(bt, 2022-12-21): This desperately needs MySQL integration tests!
|
||||
impl_enum_display_and_debug_using_to_str!(PremiumProductName);
|
||||
//impl_mysql_enum_coders!(PremiumProductName);
|
||||
//impl_mysql_from_row!(PremiumProductName);
|
||||
|
||||
/// NB: Legacy API for older code.
|
||||
impl PremiumProductName {
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::FaceAnimator => "fa",
|
||||
Self::FaceMirror => "fm",
|
||||
Self::Lipsync => "lip",
|
||||
Self::VideoStyleTransfer => "vst",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(value: &str) -> Result<Self, String> {
|
||||
match value {
|
||||
"fa" => Ok(Self::FaceAnimator),
|
||||
"fm" => Ok(Self::FaceMirror),
|
||||
"lip" => Ok(Self::Lipsync),
|
||||
"vst" => Ok(Self::VideoStyleTransfer),
|
||||
_ => Err(format!("Unknown PremiumProductName: {}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_variants() -> BTreeSet<Self> {
|
||||
// NB: BTreeSet is sorted
|
||||
// NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc.
|
||||
BTreeSet::from([
|
||||
Self::FaceAnimator,
|
||||
Self::FaceMirror,
|
||||
Self::Lipsync,
|
||||
Self::VideoStyleTransfer,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::no_table::premium_product::premium_product_name::PremiumProductName;
|
||||
use crate::test_helpers::assert_serialization;
|
||||
|
||||
mod serde {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(PremiumProductName::FaceAnimator, "fa");
|
||||
assert_serialization(PremiumProductName::FaceMirror, "fm");
|
||||
assert_serialization(PremiumProductName::Lipsync, "lip");
|
||||
assert_serialization(PremiumProductName::VideoStyleTransfer, "vst");
|
||||
}
|
||||
|
||||
mod impl_methods {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(PremiumProductName::FaceAnimator.to_str(), "fa");
|
||||
assert_eq!(PremiumProductName::FaceMirror.to_str(), "fm");
|
||||
assert_eq!(PremiumProductName::Lipsync.to_str(), "lip");
|
||||
assert_eq!(PremiumProductName::VideoStyleTransfer.to_str(), "vst");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(PremiumProductName::from_str("fa").unwrap(), PremiumProductName::FaceAnimator);
|
||||
assert_eq!(PremiumProductName::from_str("fm").unwrap(), PremiumProductName::FaceMirror);
|
||||
assert_eq!(PremiumProductName::from_str("lip").unwrap(), PremiumProductName::Lipsync);
|
||||
assert_eq!(PremiumProductName::from_str("vst").unwrap(), PremiumProductName::VideoStyleTransfer);
|
||||
}
|
||||
}
|
||||
|
||||
mod manual_variant_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let variants = PremiumProductName::all_variants();
|
||||
assert_eq!(variants.len(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
mod mechanical_checks {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn variant_length() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(PremiumProductName::all_variants().len(), PremiumProductName::iter().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
for variant in PremiumProductName::all_variants() {
|
||||
assert_eq!(variant, PremiumProductName::from_str(variant.to_str()).unwrap());
|
||||
assert_eq!(variant, PremiumProductName::from_str(&format!("{}", variant)).unwrap());
|
||||
assert_eq!(variant, PremiumProductName::from_str(&format!("{:?}", variant)).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ normal = ["my-workspace-hack"]
|
||||
|
||||
[dependencies]
|
||||
# Internal
|
||||
enums = { path = "../../_types/enums" }
|
||||
errors = { path = "../errors" }
|
||||
tokens = { path = "../../_types/tokens" }
|
||||
|
||||
@@ -26,5 +27,8 @@ my-workspace-hack = { version = "0.1", path = "../../my-workspace-hack" }
|
||||
# External
|
||||
chrono = { version = "0.4.22", features = ["serde"] }
|
||||
|
||||
# Redis
|
||||
r2d2_redis = "0.14.0"
|
||||
|
||||
[dev-dependencies]
|
||||
# None yet
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod premium_avt_redis_key;
|
||||
pub mod premium_ip_address_redis_key;
|
||||
pub mod premium_user_token_redis_key;
|
||||
pub mod premium_user_redis_key;
|
||||
@@ -5,7 +5,8 @@ pub struct PremiumAvtRedisKey(pub String);
|
||||
|
||||
impl_string_key!(PremiumAvtRedisKey);
|
||||
|
||||
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
|
||||
// NB: 62 days to last over a month with enough time for debugging.
|
||||
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
|
||||
|
||||
impl PremiumAvtRedisKey {
|
||||
pub fn new_for_user(avt: &AnonymousVisitorTrackingToken, time: DateTime<Utc>) -> Self {
|
||||
@@ -15,7 +16,7 @@ impl PremiumAvtRedisKey {
|
||||
}
|
||||
|
||||
pub fn get_redis_ttl() -> Duration {
|
||||
DURATION
|
||||
REDIS_KEY_TTL_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +36,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert_eq!(PremiumAvtRedisKey::get_redis_ttl().num_days(), 32);
|
||||
assert_eq!(PremiumAvtRedisKey::get_redis_ttl().num_days(), 62);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ pub struct PremiumIpAddressRedisKey(pub String);
|
||||
|
||||
impl_string_key!(PremiumIpAddressRedisKey);
|
||||
|
||||
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
|
||||
// NB: 62 days to last over a month with enough time for debugging.
|
||||
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
|
||||
|
||||
impl PremiumIpAddressRedisKey {
|
||||
pub fn new_for_user(ip_addr: &str, time: DateTime<Utc>) -> Self {
|
||||
@@ -14,7 +15,7 @@ impl PremiumIpAddressRedisKey {
|
||||
}
|
||||
|
||||
pub fn get_redis_ttl() -> Duration {
|
||||
DURATION
|
||||
REDIS_KEY_TTL_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +34,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert_eq!(PremiumIpAddressRedisKey::get_redis_ttl().num_days(), 32);
|
||||
assert_eq!(PremiumIpAddressRedisKey::get_redis_ttl().num_days(), 62);
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -2,13 +2,14 @@ use chrono::{Datelike, DateTime, Duration, Utc};
|
||||
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct PremiumUserTokenRedisKey(pub String);
|
||||
pub struct PremiumUserRedisKey(pub String);
|
||||
|
||||
impl_string_key!(PremiumUserTokenRedisKey);
|
||||
impl_string_key!(PremiumUserRedisKey);
|
||||
|
||||
const DURATION : Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 32); // 32 days
|
||||
// NB: 62 days to last over a month with enough time for debugging.
|
||||
const REDIS_KEY_TTL_DURATION: Duration = Duration::milliseconds(1000 * 60 * 60 * 24 * 62);
|
||||
|
||||
impl PremiumUserTokenRedisKey {
|
||||
impl PremiumUserRedisKey {
|
||||
pub fn new_for_user(user_token: &UserToken, time: DateTime<Utc>) -> Self {
|
||||
let month = time.month();
|
||||
let key = format!("premium:user:{}:{}", user_token.as_str(), month);
|
||||
@@ -16,7 +17,7 @@ impl PremiumUserTokenRedisKey {
|
||||
}
|
||||
|
||||
pub fn get_redis_ttl() -> Duration {
|
||||
DURATION
|
||||
REDIS_KEY_TTL_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +31,12 @@ mod tests {
|
||||
fn test_new_for_user() {
|
||||
let user_token = UserToken::new_from_str("token");
|
||||
let time = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap();
|
||||
let key = PremiumUserTokenRedisKey::new_for_user(&user_token, time);
|
||||
let key = PremiumUserRedisKey::new_for_user(&user_token, time);
|
||||
assert_eq!(key.as_str(), "premium:user:token:1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert_eq!(PremiumUserTokenRedisKey::get_redis_ttl().num_days(), 32);
|
||||
assert_eq!(PremiumUserRedisKey::get_redis_ttl().num_days(), 62);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod premium;
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod premium_user_payload;
|
||||
|
||||
// Private
|
||||
pub (crate) mod product_by_week_store;
|
||||
pub (crate) mod product_by_week_subkey;
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Add;
|
||||
use chrono::Utc;
|
||||
use r2d2_redis::r2d2::PooledConnection;
|
||||
use r2d2_redis::redis::{Commands};
|
||||
use r2d2_redis::RedisConnectionManager;
|
||||
use errors::AnyhowResult;
|
||||
use crate::keys::premium::premium_user_redis_key::PremiumUserRedisKey;
|
||||
|
||||
pub struct PremiumUserPayload {
|
||||
key: PremiumUserRedisKey,
|
||||
|
||||
credits_used: Option<u64>,
|
||||
|
||||
}
|
||||
|
||||
impl PremiumUserPayload {
|
||||
|
||||
pub fn read_from_redis(key: PremiumUserRedisKey, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<Self> {
|
||||
let _values : HashMap<String, String> = redis.hgetall(key.as_str())?;
|
||||
|
||||
let mut free_uses_per_product_map = HashMap::new();
|
||||
|
||||
let keys: Vec<String> = redis.hkeys(key.as_str())?;
|
||||
|
||||
for key in keys {
|
||||
if key == "credits" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let count: u64 = redis.hget(key.as_str(), "count")?;
|
||||
|
||||
free_uses_per_product_map.insert(key, count);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
key,
|
||||
credits_used: Some(0), // TODO
|
||||
//free_uses_per_product_map,
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn persist_to_redis(&self, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<()> {
|
||||
// let mut values = Vec::with_capacity(self.free_uses_per_product_map.len() + 1);
|
||||
//
|
||||
// values.push(("credits", self.credits_used.unwrap_or(0).to_string()));
|
||||
//
|
||||
// for (product_by_week_key, count) in &self.free_uses_per_product_map {
|
||||
// values.push((product_by_week_key, count.to_string()));
|
||||
// }
|
||||
//
|
||||
// redis.hset_multiple(self.key.as_str(), &values)?;
|
||||
//
|
||||
// self.set_key_expiry(redis)?;
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
pub fn set_key_expiry(&self, mut redis: PooledConnection<RedisConnectionManager>) -> AnyhowResult<()> {
|
||||
let expire_at= Utc::now()
|
||||
.add(PremiumUserRedisKey::get_redis_ttl())
|
||||
.timestamp() as usize;
|
||||
|
||||
redis.expire_at(self.key.as_str(), expire_at)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn todo() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::collections::HashMap;
|
||||
use enums::no_table::premium_product::premium_product_name::PremiumProductName;
|
||||
use crate::payloads::premium::product_by_week_subkey::ProductByWeekSubkey;
|
||||
|
||||
pub struct ProductByWeekStore {
|
||||
|
||||
/// Key: "{product_id}:{iso_week_zero_index}"
|
||||
/// Value: count of uses
|
||||
free_uses_per_product_map: HashMap<ProductByWeekSubkey, u64>,
|
||||
}
|
||||
|
||||
impl ProductByWeekStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
free_uses_per_product_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_use(&mut self, name: PremiumProductName, week: u32) {
|
||||
let key = ProductByWeekSubkey::new(name, week);
|
||||
let count = self.free_uses_per_product_map.entry(key).or_insert(0);
|
||||
*count += 1;
|
||||
}
|
||||
|
||||
pub fn get_use_count(&self, name: PremiumProductName, week: u32) -> u64 {
|
||||
let key = ProductByWeekSubkey::new(name, week);
|
||||
*self.free_uses_per_product_map.get(&key).unwrap_or(&0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_increment_use() {
|
||||
let mut store = ProductByWeekStore::new();
|
||||
|
||||
store.increment_use(PremiumProductName::FaceAnimator, 10);
|
||||
store.increment_use(PremiumProductName::FaceAnimator, 10);
|
||||
store.increment_use(PremiumProductName::FaceAnimator, 10);
|
||||
store.increment_use(PremiumProductName::FaceMirror, 10);
|
||||
store.increment_use(PremiumProductName::Lipsync, 11);
|
||||
|
||||
// Week 10
|
||||
assert_eq!(store.get_use_count(PremiumProductName::FaceAnimator, 10), 3);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::FaceMirror, 10), 1);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::Lipsync, 10), 0);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::VideoStyleTransfer, 10), 0);
|
||||
|
||||
// Week 11
|
||||
assert_eq!(store.get_use_count(PremiumProductName::FaceAnimator, 11), 0);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::FaceMirror, 11), 0);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::Lipsync, 11), 1);
|
||||
assert_eq!(store.get_use_count(PremiumProductName::VideoStyleTransfer, 11), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::num::ParseIntError;
|
||||
use chrono::{Datelike, Utc};
|
||||
use enums::no_table::premium_product::premium_product_name::PremiumProductName;
|
||||
use errors::{anyhow, AnyhowResult};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ProductByWeekSubkey {
|
||||
pub product_name: PremiumProductName,
|
||||
pub week: u32,
|
||||
}
|
||||
|
||||
impl ProductByWeekSubkey {
|
||||
pub fn new(product_name: PremiumProductName, week: u32) -> Self {
|
||||
Self { product_name, week }
|
||||
}
|
||||
|
||||
pub fn new_this_week(product_name: PremiumProductName) -> Self {
|
||||
let time = Utc::now();
|
||||
let week = time.iso_week();
|
||||
let week = week.week0();
|
||||
Self { product_name, week }
|
||||
}
|
||||
|
||||
pub fn from_string(key: &str) -> AnyhowResult<Self> {
|
||||
let mut parts = key.split(':');
|
||||
let product_name = parts.next()
|
||||
.ok_or_else(|| anyhow!("No product name"))?;
|
||||
let product_name = PremiumProductName::from_str(product_name)
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
let week = parts.next()
|
||||
.ok_or_else(|| anyhow!("No week"))?
|
||||
.parse()
|
||||
.map_err(|err: ParseIntError| anyhow!(err))?;
|
||||
Ok(Self {
|
||||
product_name,
|
||||
week,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
format!("{}:{}", self.product_name.to_str(), self.week)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_this_week() {
|
||||
let key = ProductByWeekSubkey::new_this_week(PremiumProductName::FaceAnimator);
|
||||
assert_eq!(key.product_name, PremiumProductName::FaceAnimator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let key = ProductByWeekSubkey::from_string("fa:1").unwrap();
|
||||
assert_eq!(key.product_name, PremiumProductName::FaceAnimator);
|
||||
assert_eq!(key.week, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
let key = ProductByWeekSubkey::new(PremiumProductName::FaceMirror, 2);
|
||||
assert_eq!(key.to_string(), "fm:2");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user