gitv 0.1.0

A git repos analyzing and visualizing tool built in Rust.
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::{collections::HashMap, fs::File};

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAction {
    pub disable_pull: Option<bool>,
    pub author_mappings: Option<Vec<AuthorMapping>>,
    pub databases: Vec<Database>,
}

#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
pub struct AuthorMapping {
    pub source: Author,
    pub destination: Author,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Repository {
    pub name: String,
    pub branch: Option<String>,
    pub remote: Option<String>,
    pub path: String,
    pub forks_count: Option<usize>,
    pub stargazers_count: Option<usize>,
}

#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Hash)]
pub struct Author {
    pub name: String,
    pub email: String,
}

impl Author {
    pub fn domain(&self) -> String {
        let email = self.email.clone();
        let fields = email.splitn(2, '@').collect::<Vec<&str>>();
        fields.last().unwrap_or(&"").to_string()
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Database {
    pub dir: String,
    pub files: Option<Vec<String>>,
    pub repos: Option<Vec<Repository>>,
}

impl Database {
    pub fn load(&self) -> Result<Vec<Repository>> {
        let mut repos = vec![];
        if self.repos.is_some() {
            repos.extend(self.repos.clone().unwrap());
        }

        if self.files.is_some() {
            for file in self.files.clone().unwrap() {
                let f = File::open(&file)?;
                let r: Vec<Repository> = serde_yaml::from_reader(f)?;
                repos.extend(r);
            }
        }
        Ok(repos)
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchAction {
    pub github_authenticated: Option<Vec<GithubAuthenticated>>,
    pub github_user: Option<Vec<GithubUser>>,
    pub github_org: Option<Vec<GithubOrg>>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubAuthenticated {
    pub clone_dir: String,
    pub destination: String,
    pub token: String,
    pub exclude_orgs: Option<Vec<String>>,
    pub exclude_repos: Option<Vec<String>>,
    pub visibility: Option<String>,
    pub affiliation: Option<String>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubUser {
    pub clone_dir: String,
    pub destination: String,
    pub token: String,
    pub username: String,
    pub exclude_repos: Option<Vec<String>>,
    #[serde(rename(serialize = "type", deserialize = "type"))]
    pub typ: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GithubOrg {
    pub clone_dir: String,
    pub destination: String,
    pub token: String,
    pub org: String,
    pub exclude_repos: Option<Vec<String>>,
    #[serde(rename(serialize = "type", deserialize = "type"))]
    pub typ: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct ShellAction {
    pub executions: Vec<Execution>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Execution {
    pub db_name: String,
    pub dir: String,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct RenderAction {
    pub executions: Vec<Execution>,
    pub display: Display,
    pub colors: Option<HashMap<String, Vec<Value>>>,
    pub functions: Option<HashMap<String, Value>>,
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Display {
    pub destination: String,
    pub render_mode: String,
    pub dependency: Option<Dependency>,
    pub queries: Vec<Query>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Dependency {
    chartjs: String,
    datalabels: String,
}

static REGISTER_DATALABELS: &str = "Chart.register(ChartDataLabels)";

impl Dependency {
    pub fn list(&self) -> Vec<String> {
        let mut data = vec![];
        if self.chartjs.is_empty() {
            data.push(Self::default().chartjs);
        } else {
            data.push(self.chartjs.clone())
        }
        if !self.datalabels.is_empty() {
            data.push(self.datalabels.clone())
        }
        data
    }

    pub fn register(&self) -> Vec<&'static str> {
        let mut data = vec![];
        if !self.datalabels.is_empty() {
            data.push(REGISTER_DATALABELS);
        }
        data
    }
}

static DEPENDENCY_CHARTJS: &str = "https://cdn.bootcdn.net/ajax/libs/Chart.js/3.7.1/chart.min.js";
static DEPENDENCY_DATALABELS: &str = "https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0";

impl Default for Dependency {
    fn default() -> Self {
        Self {
            chartjs: String::from(DEPENDENCY_CHARTJS),
            datalabels: String::from(DEPENDENCY_DATALABELS),
        }
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Query {
    pub statements: Vec<String>,
    pub chart: Option<ChartConfig>,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct ChartConfig {
    #[serde(rename(deserialize = "type"))]
    pub chart_type: String,
    pub width: String,
    pub height: String,
    pub name: String,
    pub options: Option<Value>,
    pub data: Value,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
    pub create: Option<CreateAction>,
    pub fetch: Option<FetchAction>,
    pub shell: Option<ShellAction>,
    pub render: Option<RenderAction>,
}

pub fn load_config(c: &str) -> Result<Config> {
    let f = File::open(c)?;
    let config: Config = serde_yaml::from_reader(f)?;
    Ok(config)
}