pub mod config;
pub mod error;
pub mod github;
pub use config::{GITHUB_API_URL, GITHUB_GRAPHQL_URL};
pub use error::{GitHubError, Result};
pub use github::{
GitHubClient, GitHubService, RateLimit, Repository, SearchRepository, StargazerWithDate, User,
UserProfile,
};
use base64::Engine;
pub fn parse_github_node_id(node_id: &str) -> i64 {
if let Some(b64_part) = node_id.strip_prefix("R_") {
let padded = match b64_part.len() % 4 {
2 => format!("{}==", b64_part),
3 => format!("{}=", b64_part),
_ => b64_part.to_string(),
};
if let Ok(bytes) = base64::engine::general_purpose::URL_SAFE.decode(&padded) {
if bytes.len() >= 7 && bytes[0] == 0x92 && bytes[2] == 0xce {
let id_bytes = &bytes[3..7];
return i64::from(u32::from_be_bytes([
id_bytes[0],
id_bytes[1],
id_bytes[2],
id_bytes[3],
]));
}
}
}
if node_id.starts_with("MDEw")
&& let Some(id) = base64::engine::general_purpose::STANDARD
.decode(node_id)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
.and_then(|s| s.split(':').next_back().and_then(|n| n.parse::<i64>().ok()))
{
return id;
}
tracing::warn!("Failed to parse GitHub node ID: {}", node_id);
0
}
pub fn parse_repository(repo: &str) -> std::result::Result<(String, String), String> {
let parts: Vec<&str> = repo.split('/').collect();
if parts.len() != 2 {
return Err(format!(
"Invalid repository format '{}'. Use 'owner/repo'",
repo
));
}
let owner = parts[0].trim();
let name = parts[1].trim();
if owner.is_empty() || name.is_empty() {
return Err(format!(
"Invalid repository format '{}'. Owner and repository name cannot be empty",
repo
));
}
Ok((owner.to_string(), name.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_repository_valid() {
let (owner, repo) = parse_repository("owner/repo").unwrap();
assert_eq!(owner, "owner");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_repository_with_whitespace() {
let (owner, repo) = parse_repository(" owner / repo ").unwrap();
assert_eq!(owner, "owner");
assert_eq!(repo, "repo");
}
#[test]
fn test_parse_repository_invalid() {
assert!(parse_repository("invalid").is_err());
assert!(parse_repository("/repo").is_err());
assert!(parse_repository("owner/").is_err());
assert!(parse_repository("owner/repo/extra").is_err());
}
#[test]
fn test_parse_github_node_id_new_format() {
assert_eq!(parse_github_node_id("R_kgDOQBnJRQ"), 1075431749);
assert_eq!(parse_github_node_id("R_kgDOQpc_vg"), 1117208510);
assert_eq!(parse_github_node_id("R_kgDOPQG3Mw"), 1023522611);
}
#[test]
fn test_parse_github_node_id_invalid() {
assert_eq!(parse_github_node_id("invalid"), 0);
assert_eq!(parse_github_node_id(""), 0);
assert_eq!(parse_github_node_id("R_invalid"), 0);
}
}