use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use crate::types::{BroadcasterId, Cost, TransactionId, UserId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BitsLeaderboard {
pub user_id: UserId,
pub user_login: String,
pub user_name: String,
pub rank: u64,
pub score: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionTransaction {
pub id: TransactionId,
pub timestamp: String,
pub broadcaster_id: BroadcasterId,
pub broadcaster_login: String,
pub broadcaster_name: String,
pub user_id: UserId,
pub user_login: String,
pub user_name: String,
pub product_type: String,
pub product_data: TransactionProductData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cheermotes {
pub prefix: String,
pub tiers: Vec<Tier>,
#[serde(rename = "type")]
pub kind: Type,
pub order: u64,
pub last_updated: DateTime<FixedOffset>,
pub is_charitable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tier {
pub min_bits: u64,
pub id: TierLevel,
pub color: String,
pub images: Images,
pub can_cheer: bool,
pub show_in_bits_card: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TierLevel {
#[serde(rename = "1")]
One,
#[serde(rename = "10")]
Ten,
#[serde(rename = "100")]
Hundred,
#[serde(rename = "500")]
FiveHundred,
#[serde(rename = "1000")]
Thousand,
#[serde(rename = "5000")]
FiveThousand,
#[serde(rename = "10000")]
TenThousand,
#[serde(rename = "100000")]
OneHundredThousand,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Type {
GlobalFirstParty,
GlobalThirdParty,
ChannelCustom,
DisplayOnly,
Sponsored,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Images {
pub dark: Dark,
pub light: Light,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dark {
pub animated: Imagess,
#[serde(rename = "static")]
pub static_image: Imagess,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Light {
pub animated: Imagess,
#[serde(rename = "static")]
pub static_image: Imagess,
}
#[allow(non_snake_case)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Imagess {
#[serde(rename(serialize = "1", deserialize = "1"))]
pub One: String,
#[serde(rename(serialize = "1.5", deserialize = "1.5"))]
pub OneDotFive: String,
#[serde(rename(serialize = "2", deserialize = "2"))]
pub Two: String,
#[serde(rename(serialize = "3", deserialize = "3"))]
pub Three: String,
#[serde(rename(serialize = "4", deserialize = "4"))]
pub Four: String,
}
#[allow(non_snake_case)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionProductData {
pub domain: String,
pub sku: String,
pub cost: Cost,
pub inDevelopment: bool,
pub displayName: String,
pub expiration: String,
pub broadcast: bool,
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
pub enum ProductType {
BITS_IN_EXTENSION,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Period {
Day,
Week,
Month,
Year,
All,
}
impl Period {
pub fn as_str(&self) -> &str {
match self {
Self::Day => "day",
Self::Week => "week",
Self::Month => "month",
Self::Year => "year",
Self::All => "all",
}
}
}
impl AsRef<str> for Period {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use chrono::{DateTime, FixedOffset, Timelike};
use serde_json::json;
use crate::bits::{Cheermotes, Images, TierLevel, TransactionProductData, Type};
#[test]
fn tier_level_enum() {
let tiers = vec![
(TierLevel::One, "1"),
(TierLevel::Ten, "10"),
(TierLevel::Hundred, "100"),
(TierLevel::FiveHundred, "500"),
(TierLevel::Thousand, "1000"),
(TierLevel::FiveThousand, "5000"),
(TierLevel::TenThousand, "10000"),
(TierLevel::OneHundredThousand, "100000"),
];
for (tier, expected_value) in tiers {
let serialized = serde_json::to_string(&tier).unwrap();
assert_eq!(serialized, format!("\"{}\"", expected_value));
let deserialized: TierLevel = serde_json::from_str(&serialized).unwrap();
let re_serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(serialized, re_serialized);
}
}
#[test]
fn cheermote_type_enum() {
let types = vec![
Type::GlobalFirstParty,
Type::GlobalThirdParty,
Type::ChannelCustom,
Type::DisplayOnly,
Type::Sponsored,
];
for cheermote_type in types {
let serialized = serde_json::to_string(&cheermote_type).unwrap();
let deserialized: Type = serde_json::from_str(&serialized).unwrap();
let re_serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(serialized, re_serialized);
}
}
#[test]
fn image_structure_naming_conventions() {
let images_json = json!({
"dark": {
"animated": {
"1": "https://example.com/1.gif",
"1.5": "https://example.com/1.5.gif",
"2": "https://example.com/2.gif",
"3": "https://example.com/3.gif",
"4": "https://example.com/4.gif"
},
"static": {
"1": "https://example.com/1.png",
"1.5": "https://example.com/1.5.png",
"2": "https://example.com/2.png",
"3": "https://example.com/3.png",
"4": "https://example.com/4.png"
}
},
"light": {
"animated": {
"1": "https://example.com/light/1.gif",
"1.5": "https://example.com/light/1.5.gif",
"2": "https://example.com/light/2.gif",
"3": "https://example.com/light/3.gif",
"4": "https://example.com/light/4.gif"
},
"static": {
"1": "https://example.com/light/1.png",
"1.5": "https://example.com/light/1.5.png",
"2": "https://example.com/light/2.png",
"3": "https://example.com/light/3.png",
"4": "https://example.com/light/4.png"
}
}
});
let images: Images = serde_json::from_value(images_json).unwrap();
assert_eq!(images.dark.animated.One, "https://example.com/1.gif");
assert_eq!(
images.dark.animated.OneDotFive,
"https://example.com/1.5.gif"
);
assert_eq!(images.dark.static_image.Two, "https://example.com/2.png");
assert_eq!(
images.light.animated.Three,
"https://example.com/light/3.gif"
);
assert_eq!(
images.light.static_image.Four,
"https://example.com/light/4.png"
);
}
#[test]
fn transaction_product_data_naming() {
let product_data_json = json!({
"domain": "twitch-ext",
"sku": "test-sku",
"cost": {
"amount": 100,
"type": "bits"
},
"inDevelopment": false,
"displayName": "Test Product",
"expiration": "2024-12-01T15:30:00Z",
"broadcast": true
});
let product_data: TransactionProductData =
serde_json::from_value(product_data_json).unwrap();
assert_eq!(product_data.domain, "twitch-ext");
assert_eq!(product_data.sku, "test-sku");
assert_eq!(product_data.cost.amount, 100);
assert!(!product_data.inDevelopment);
assert_eq!(product_data.displayName, "Test Product");
assert!(product_data.broadcast);
}
#[test]
fn date_time_parsing_in_cheermotes() {
let datetime_str = "2023-12-01T15:30:00Z";
let parsed = DateTime::<FixedOffset>::from_str(datetime_str).unwrap();
assert_eq!(parsed.hour(), 15);
assert_eq!(parsed.minute(), 30);
let cheermote_json = json!({
"prefix": "Cheer",
"tiers": [],
"type": "global_first_party",
"order": 1,
"last_updated": datetime_str,
"is_charitable": false
});
let cheermote: Cheermotes = serde_json::from_value(cheermote_json).unwrap();
assert_eq!(cheermote.last_updated.hour(), 15);
}
}