runtime-cli 0.1.0

Command-line client for managing git projects on Runtime
Documentation
//! `runtime org` — list / members / teams.

use std::io::Write;

use anyhow::{anyhow, Result};
use clap::{Args, Subcommand};
use serde_json::{json, Value};

use crate::client;
use crate::output::{render_table, FormatChoice};

#[derive(Debug, Subcommand)]
pub enum OrgCmd {
    /// List organizations the viewer is a member of.
    List(FormatArgs),
    /// List members of an organization.
    Members(LoginArgs),
    /// List teams in an organization.
    Teams(LoginArgs),
}

#[derive(Debug, Args)]
pub struct FormatArgs {
    #[arg(long, default_value_t = false)]
    pub json: bool,
    #[arg(long, default_value_t = false)]
    pub tsv: bool,
}

#[derive(Debug, Args)]
pub struct LoginArgs {
    pub login: String,
    #[arg(long, default_value_t = false)]
    pub json: bool,
    #[arg(long, default_value_t = false)]
    pub tsv: bool,
}

pub fn run(cmd: OrgCmd) -> Result<()> {
    match cmd {
        OrgCmd::List(a) => list(a),
        OrgCmd::Members(a) => members(a),
        OrgCmd::Teams(a) => teams(a),
    }
}

fn list(args: FormatArgs) -> Result<()> {
    let (cli, _cfg) = client::authenticated()?;
    let data = cli.execute(
        "{ viewer { organizations { login displayName description } } }",
        json!({}),
    )?;
    let orgs = data["viewer"]["organizations"]
        .as_array()
        .cloned()
        .unwrap_or_default();
    let rows = org_rows(&orgs);
    let fmt = FormatChoice {
        json: args.json,
        tsv: args.tsv,
    }
    .resolve();
    std::io::stdout()
        .lock()
        .write_all(render_table(&["login", "name", "description"], &rows, fmt).as_bytes())?;
    Ok(())
}

fn members(args: LoginArgs) -> Result<()> {
    let (cli, _cfg) = client::authenticated()?;
    let data = cli.execute(
        "query($login: String!) { organization(login: $login) { members { role account { username displayName } } } }",
        json!({ "login": args.login }),
    )?;
    let org = &data["organization"];
    if org.is_null() {
        return Err(anyhow!("no organization `{}`", args.login));
    }
    let members = org["members"].as_array().cloned().unwrap_or_default();
    let rows = member_rows(&members);
    let fmt = FormatChoice {
        json: args.json,
        tsv: args.tsv,
    }
    .resolve();
    std::io::stdout()
        .lock()
        .write_all(render_table(&["user", "name", "role"], &rows, fmt).as_bytes())?;
    Ok(())
}

fn teams(args: LoginArgs) -> Result<()> {
    let (cli, _cfg) = client::authenticated()?;
    let data = cli.execute(
        "query($login: String!) { organization(login: $login) { teams { slug name description } } }",
        json!({ "login": args.login }),
    )?;
    let org = &data["organization"];
    if org.is_null() {
        return Err(anyhow!("no organization `{}`", args.login));
    }
    let teams = org["teams"].as_array().cloned().unwrap_or_default();
    let rows = team_rows(&teams);
    let fmt = FormatChoice {
        json: args.json,
        tsv: args.tsv,
    }
    .resolve();
    std::io::stdout()
        .lock()
        .write_all(render_table(&["slug", "name", "description"], &rows, fmt).as_bytes())?;
    Ok(())
}

fn org_rows(orgs: &[Value]) -> Vec<Vec<String>> {
    orgs.iter()
        .map(|o| {
            vec![
                o["login"].as_str().unwrap_or("").to_string(),
                o["displayName"].as_str().unwrap_or("").to_string(),
                o["description"].as_str().unwrap_or("").to_string(),
            ]
        })
        .collect()
}

fn member_rows(members: &[Value]) -> Vec<Vec<String>> {
    members
        .iter()
        .map(|m| {
            vec![
                m["account"]["username"].as_str().unwrap_or("").to_string(),
                m["account"]["displayName"]
                    .as_str()
                    .unwrap_or("")
                    .to_string(),
                m["role"].as_str().unwrap_or("").to_string(),
            ]
        })
        .collect()
}

fn team_rows(teams: &[Value]) -> Vec<Vec<String>> {
    teams
        .iter()
        .map(|t| {
            vec![
                t["slug"].as_str().unwrap_or("").to_string(),
                t["name"].as_str().unwrap_or("").to_string(),
                t["description"].as_str().unwrap_or("").to_string(),
            ]
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn org_rows_format_org_member_and_team_tables_with_defaults() {
        let orgs = vec![
            json!({
                "login": "runtime-labs",
                "displayName": "Runtime Labs",
                "description": "Builds Runtime"
            }),
            json!({}),
        ];
        assert_eq!(
            org_rows(&orgs),
            vec![
                vec!["runtime-labs", "Runtime Labs", "Builds Runtime"],
                vec!["", "", ""],
            ]
        );

        let members = vec![
            json!({
                "role": "ADMIN",
                "account": { "username": "bri", "displayName": "Bri" }
            }),
            json!({}),
        ];
        assert_eq!(
            member_rows(&members),
            vec![vec!["bri", "Bri", "ADMIN"], vec!["", "", ""]]
        );

        let teams = vec![
            json!({ "slug": "ops", "name": "Ops", "description": "Operators" }),
            json!({}),
        ];
        assert_eq!(
            team_rows(&teams),
            vec![vec!["ops", "Ops", "Operators"], vec!["", "", ""]]
        );
    }
}