use crate::models::Snowflake;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApiResponse<T> {
#[serde(flatten)]
pub data: T,
pub code: Option<u32>,
pub message: Option<String>,
}
impl<T> ApiResponse<T> {
pub fn success(data: T) -> Self {
Self {
data,
code: None,
message: None,
}
}
pub fn error(code: u32, message: impl Into<String>) -> Self
where
T: Default,
{
Self {
data: T::default(),
code: Some(code),
message: Some(message.into()),
}
}
pub fn is_success(&self) -> bool {
self.code.is_none()
}
pub fn is_error(&self) -> bool {
self.code.is_some()
}
pub fn into_result(self) -> crate::Result<T> {
if let Some(code) = self.code {
let message = self.message.unwrap_or_else(|| format!("API error {code}"));
Err(crate::BotError::api(code, message))
} else {
Ok(self.data)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GatewayResponse {
pub url: String,
pub shards: u32,
pub session_start_limit: SessionStartLimit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionStartLimit {
pub total: u32,
pub remaining: u32,
pub reset_after: u64,
pub max_concurrency: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BotInfo {
pub id: Snowflake,
pub username: String,
pub avatar: Option<String>,
#[serde(default)]
pub bot: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pagination {
pub page: u32,
pub per_page: u32,
pub total: u32,
pub total_pages: u32,
pub has_next: bool,
pub has_prev: bool,
}
impl Pagination {
pub fn new(page: u32, per_page: u32, total: u32) -> Self {
let total_pages = total.div_ceil(per_page); Self {
page,
per_page,
total,
total_pages,
has_next: page < total_pages,
has_prev: page > 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaginatedResponse<T> {
pub items: Vec<T>,
pub pagination: Pagination,
}
impl<T> PaginatedResponse<T> {
pub fn new(items: Vec<T>, pagination: Pagination) -> Self {
Self { items, pagination }
}
pub fn has_more(&self) -> bool {
self.pagination.has_next
}
pub fn next_page(&self) -> Option<u32> {
if self.pagination.has_next {
Some(self.pagination.page + 1)
} else {
None
}
}
pub fn prev_page(&self) -> Option<u32> {
if self.pagination.has_prev {
Some(self.pagination.page - 1)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimit {
pub bucket: Option<String>,
pub limit: u32,
pub remaining: u32,
pub reset: u64,
pub retry_after: Option<u64>,
}
impl RateLimit {
pub fn is_exceeded(&self) -> bool {
self.remaining == 0
}
pub fn reset_in(&self) -> u64 {
let now = chrono::Utc::now().timestamp() as u64;
self.reset.saturating_sub(now)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApiError {
pub code: u32,
pub message: String,
pub errors: Option<serde_json::Value>,
pub trace_id: Option<String>,
}
impl ApiError {
pub fn new(code: u32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
errors: None,
trace_id: None,
}
}
pub fn is_rate_limit(&self) -> bool {
self.code == 429
}
pub fn is_auth_error(&self) -> bool {
self.code == 401 || self.code == 403
}
pub fn is_not_found(&self) -> bool {
self.code == 404
}
pub fn is_server_error(&self) -> bool {
self.code >= 500
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "API Error {}: {}", self.code, self.message)
}
}
impl std::error::Error for ApiError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AudioAction {
pub guild_id: Option<String>,
pub channel_id: Option<String>,
pub audio_url: Option<String>,
pub text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MessageResponse {
pub id: Option<Snowflake>,
pub timestamp: Option<String>,
#[serde(flatten)]
pub extra: Option<serde_json::Value>,
}
impl MessageResponse {
pub fn new(id: impl Into<Snowflake>) -> Self {
Self {
id: Some(id.into()),
timestamp: Some(chrono::Utc::now().to_rfc3339()),
extra: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_response() {
let success: ApiResponse<String> = ApiResponse::success("test".to_string());
assert!(success.is_success());
assert!(!success.is_error());
assert!(success.into_result().is_ok());
let error: ApiResponse<String> = ApiResponse::error(404, "Not found");
assert!(!error.is_success());
assert!(error.is_error());
assert!(error.into_result().is_err());
}
#[test]
fn test_pagination() {
let pagination = Pagination::new(2, 10, 25);
assert_eq!(pagination.total_pages, 3);
assert!(pagination.has_prev);
assert!(pagination.has_next);
let last_page = Pagination::new(3, 10, 25);
assert!(!last_page.has_next);
assert!(last_page.has_prev);
}
#[test]
fn test_rate_limit() {
let rate_limit = RateLimit {
bucket: Some("global".to_string()),
limit: 100,
remaining: 0,
reset: chrono::Utc::now().timestamp() as u64 + 60,
retry_after: Some(60),
};
assert!(rate_limit.is_exceeded());
assert!(rate_limit.reset_in() > 0);
}
#[test]
fn test_api_error() {
let error = ApiError::new(429, "Rate limited");
assert!(error.is_rate_limit());
assert!(!error.is_auth_error());
assert!(!error.is_not_found());
assert!(!error.is_server_error());
let auth_error = ApiError::new(401, "Unauthorized");
assert!(auth_error.is_auth_error());
}
}