pub mod client;
#[allow(clippy::module_inception)]
pub mod db;
pub mod usage;
pub use client::{BirdClient, CacheOpts, RequestContext};
#[allow(unused_imports)]
pub(crate) use db::{BirdDb, BookmarkRow, RawResponseRow, StoreStats, TweetRow, UserRow};
pub use usage::{ActualUsageDay, DailyUsage, EndpointUsage, UsageLogEntry, UsageSummary};
use std::time::{SystemTime, UNIX_EPOCH};
const KNOWN_LITERALS: &[&str] = &[
"2",
"tweets",
"users",
"search",
"recent",
"bookmarks",
"me",
"by",
"username",
"usage",
"oauth2",
"token",
"compliance",
"lists",
"spaces",
"dm_conversations",
];
pub fn normalize_endpoint(url: &str) -> String {
let path = url::Url::parse(url)
.map(|u| u.path().to_string())
.unwrap_or_else(|_| url.to_string());
let segments: Vec<&str> = path.split('/').collect();
let mut normalized = Vec::with_capacity(segments.len());
let mut prev_two: (Option<&str>, Option<&str>) = (None, None);
for seg in &segments {
if seg.is_empty() {
normalized.push(*seg);
continue;
}
if prev_two == (Some("by"), Some("username")) {
normalized.push(":username");
} else if KNOWN_LITERALS.contains(seg) {
normalized.push(seg);
} else if seg.len() >= 2 && seg.chars().all(|c| c.is_ascii_digit()) {
normalized.push(":id");
} else {
normalized.push(seg);
}
prev_two = (prev_two.1, Some(seg));
}
normalized.join("/")
}
pub(crate) fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_endpoint_search_recent() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/tweets/search/recent?query=test"),
"/2/tweets/search/recent"
);
}
#[test]
fn normalize_endpoint_users_me() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/users/me"),
"/2/users/me"
);
}
#[test]
fn normalize_endpoint_tweet_by_id() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/tweets/1234567890"),
"/2/tweets/:id"
);
}
#[test]
fn normalize_endpoint_user_bookmarks() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/users/123/bookmarks"),
"/2/users/:id/bookmarks"
);
}
#[test]
fn normalize_endpoint_username_lookup() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/users/by/username/jack"),
"/2/users/by/username/:username"
);
}
#[test]
fn normalize_endpoint_usage_tweets() {
assert_eq!(
normalize_endpoint("https://api.x.com/2/usage/tweets?usage.fields=daily_project_usage"),
"/2/usage/tweets"
);
}
#[test]
fn normalize_endpoint_strips_query() {
let url = "https://api.x.com/2/tweets/search/recent?query=rust&max_results=100";
assert_eq!(normalize_endpoint(url), "/2/tweets/search/recent");
}
}