zixcel-github 0.1.1

Policy-neutral GitHub API wrapper boundary for Zixcel
Documentation
use std::fs;
use std::process::ExitCode;

use serde::Serialize;
use serde_json::json;
use zixcel_github::{
    build_plan, capabilities, doctor, parse_api_request_json, parse_config, validate_api_request,
    validation_report,
};

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(message) => {
            let error = json!({
                "schema": "zixcel://cli-error/v1",
                "connector": zixcel_github::CONNECTOR,
                "status": "error",
                "message": message
            });
            eprintln!(
                "{}",
                serde_json::to_string_pretty(&error)
                    .unwrap_or_else(|_| "{\"status\":\"error\"}".to_owned())
            );
            ExitCode::from(2)
        }
    }
}

fn run() -> Result<(), String> {
    let mut arguments = std::env::args().skip(1);
    let command = arguments.next().ok_or_else(usage)?;
    match command.as_str() {
        "doctor" => {
            expect_no_more(&mut arguments)?;
            print_json(&doctor())
        }
        "capabilities" => {
            expect_no_more(&mut arguments)?;
            print_json(&capabilities())
        }
        "validate" => {
            let path = one_path(&mut arguments)?;
            let source = read_config(&path)?;
            let config = parse_config(&source).map_err(|error| error.to_string())?;
            print_json(&validation_report(&config).map_err(|error| error.to_string())?)
        }
        "plan" => {
            let path = one_path(&mut arguments)?;
            let source = read_config(&path)?;
            let config = parse_config(&source).map_err(|error| error.to_string())?;
            print_json(&build_plan(&config).map_err(|error| error.to_string())?)
        }
        "check-configured-request" => {
            let config_path = arguments.next().ok_or_else(usage)?;
            let request_path = arguments.next().ok_or_else(usage)?;
            expect_no_more(&mut arguments)?;
            let config = parse_config(&read_config(&config_path)?).map_err(|e| e.to_string())?;
            let request = parse_api_request_json(
                &fs::read(request_path).map_err(|_| "request file could not be read")?,
            )
            .map_err(|e| e.to_string())?;
            config
                .authorize_request(&request)
                .map_err(|e| e.to_string())?;
            print_json(&validate_api_request(&request).map_err(|e| e.to_string())?)
        }
        "check-request" => {
            let request_path = arguments.next().ok_or_else(usage)?;
            expect_no_more(&mut arguments)?;
            let request = fs::read(request_path)
                .map_err(|_| "API request file could not be read".to_owned())?;
            let request = parse_api_request_json(&request).map_err(|error| error.to_string())?;
            print_json(&validate_api_request(&request).map_err(|error| error.to_string())?)
        }
        _ => Err(usage()),
    }
}

fn one_path(arguments: &mut impl Iterator<Item = String>) -> Result<String, String> {
    let path = arguments.next().ok_or_else(usage)?;
    expect_no_more(arguments)?;
    Ok(path)
}

fn expect_no_more(arguments: &mut impl Iterator<Item = String>) -> Result<(), String> {
    if arguments.next().is_some() {
        Err(usage())
    } else {
        Ok(())
    }
}

fn read_config(path: &str) -> Result<String, String> {
    fs::read_to_string(path)
        .map_err(|_| "configuration file could not be read as UTF-8 text".to_owned())
}

fn print_json(value: &impl Serialize) -> Result<(), String> {
    println!(
        "{}",
        serde_json::to_string_pretty(value).map_err(|_| "JSON serialization failed".to_owned())?
    );
    Ok(())
}

fn usage() -> String {
    "usage: zixcel-github <doctor|capabilities|validate <config>|plan <config>|check-request <request.json>|check-configured-request <config> <request.json>>".to_owned()
}