ghfetch 0.1.0

GitHub stats in the terminal, neofetch-style
use super::client::GhClient;
use anyhow::Result;
use serde::Deserialize;

const CONTRIBUTIONS_QUERY: &str = r#"
query($login: String!) {
  user(login: $login) {
    contributionsCollection {
      totalCommitContributions
      totalPullRequestContributions
      totalIssueContributions
      totalPullRequestReviewContributions
      contributionCalendar {
        totalContributions
        weeks {
          contributionDays {
            date
            contributionCount
          }
        }
      }
    }
  }
}
"#;

const REPOS_WITH_LANGUAGES_QUERY: &str = r#"
query($login: String!, $first: Int!, $after: String) {
  user(login: $login) {
    repositories(first: $first, after: $after, ownerAffiliations: OWNER, orderBy: {field: STARGAZERS, direction: DESC}) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        name
        nameWithOwner
        description
        isFork
        isPrivate
        stargazerCount
        forkCount
        updatedAt
        diskUsage
        primaryLanguage {
          name
        }
        languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
          edges {
            size
            node {
              name
            }
          }
        }
      }
    }
  }
}
"#;

const ORG_REPOS_QUERY: &str = r#"
query($login: String!, $first: Int!, $after: String) {
  organization(login: $login) {
    repositories(first: $first, after: $after, orderBy: {field: STARGAZERS, direction: DESC}) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      nodes {
        name
        nameWithOwner
        description
        isFork
        isPrivate
        stargazerCount
        forkCount
        updatedAt
        diskUsage
        primaryLanguage {
          name
        }
        languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
          edges {
            size
            node {
              name
            }
          }
        }
      }
    }
  }
}
"#;

#[derive(Debug, Deserialize)]
pub struct ContributionsResponse {
    pub total_commits: u32,
    pub total_prs: u32,
    pub total_issues: u32,
    pub total_reviews: u32,
    pub total_contributions: u32,
    pub days: Vec<ContributionDay>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct ContributionDay {
    pub date: String,
    pub count: u32,
}

#[derive(Debug, Deserialize, Clone)]
pub struct GraphQLRepo {
    pub name: String,
    pub description: Option<String>,
    pub is_fork: bool,
    pub is_private: bool,
    pub stargazer_count: u32,
    pub fork_count: u32,
    pub disk_usage: Option<u32>,
    pub languages: Vec<(String, u64)>,
}

fn parse_repo_nodes(nodes: &serde_json::Value) -> Vec<GraphQLRepo> {
    let mut repos = Vec::new();
    if let Some(nodes) = nodes.as_array() {
        for node in nodes {
            let mut languages = Vec::new();
            if let Some(edges) = node["languages"]["edges"].as_array() {
                for edge in edges {
                    let name = edge["node"]["name"]
                        .as_str()
                        .unwrap_or_default()
                        .to_string();
                    let size = edge["size"].as_u64().unwrap_or(0);
                    languages.push((name, size));
                }
            }

            repos.push(GraphQLRepo {
                name: node["name"].as_str().unwrap_or_default().to_string(),
                description: node["description"].as_str().map(|s| s.to_string()),
                is_fork: node["isFork"].as_bool().unwrap_or(false),
                is_private: node["isPrivate"].as_bool().unwrap_or(false),
                stargazer_count: node["stargazerCount"].as_u64().unwrap_or(0) as u32,
                fork_count: node["forkCount"].as_u64().unwrap_or(0) as u32,
                disk_usage: node["diskUsage"].as_u64().map(|v| v as u32),
                languages,
            });
        }
    }
    repos
}

impl GhClient {
    pub async fn get_contributions(&self, login: &str) -> Result<ContributionsResponse> {
        let variables = serde_json::json!({ "login": login });
        let data = self.graphql(CONTRIBUTIONS_QUERY, &variables).await?;

        let collection = &data["data"]["user"]["contributionsCollection"];
        let calendar = &collection["contributionCalendar"];

        let mut days = Vec::new();
        if let Some(weeks) = calendar["weeks"].as_array() {
            for week in weeks {
                if let Some(contribution_days) = week["contributionDays"].as_array() {
                    for day in contribution_days {
                        days.push(ContributionDay {
                            date: day["date"].as_str().unwrap_or_default().to_string(),
                            count: day["contributionCount"].as_u64().unwrap_or(0) as u32,
                        });
                    }
                }
            }
        }

        Ok(ContributionsResponse {
            total_commits: collection["totalCommitContributions"].as_u64().unwrap_or(0) as u32,
            total_prs: collection["totalPullRequestContributions"]
                .as_u64()
                .unwrap_or(0) as u32,
            total_issues: collection["totalIssueContributions"].as_u64().unwrap_or(0) as u32,
            total_reviews: collection["totalPullRequestReviewContributions"]
                .as_u64()
                .unwrap_or(0) as u32,
            total_contributions: calendar["totalContributions"].as_u64().unwrap_or(0) as u32,
            days,
        })
    }

    pub async fn get_repos_with_languages(&self, login: &str) -> Result<Vec<GraphQLRepo>> {
        let mut all_repos = Vec::new();
        let mut cursor: Option<String> = None;

        loop {
            let variables = serde_json::json!({
                "login": login,
                "first": 100,
                "after": cursor,
            });

            let data = self.graphql(REPOS_WITH_LANGUAGES_QUERY, &variables).await?;

            let repos_data = &data["data"]["user"]["repositories"];
            let page_info = &repos_data["pageInfo"];
            let has_next = page_info["hasNextPage"].as_bool().unwrap_or(false);

            all_repos.extend(parse_repo_nodes(&repos_data["nodes"]));

            if has_next {
                cursor = page_info["endCursor"].as_str().map(|s| s.to_string());
            } else {
                break;
            }
        }

        Ok(all_repos)
    }

    pub async fn get_org_repos_graphql(&self, login: &str) -> Result<Vec<GraphQLRepo>> {
        let mut all_repos = Vec::new();
        let mut cursor: Option<String> = None;

        loop {
            let variables = serde_json::json!({
                "login": login,
                "first": 100,
                "after": cursor,
            });

            let data = self.graphql(ORG_REPOS_QUERY, &variables).await?;

            let repos_data = &data["data"]["organization"]["repositories"];
            let page_info = &repos_data["pageInfo"];
            let has_next = page_info["hasNextPage"].as_bool().unwrap_or(false);

            all_repos.extend(parse_repo_nodes(&repos_data["nodes"]));

            if has_next {
                cursor = page_info["endCursor"].as_str().map(|s| s.to_string());
            } else {
                break;
            }
        }

        Ok(all_repos)
    }
}