cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use clap::ArgMatches;

use crate::infra::driven::fs::config::SourceConfig;
use crate::infra::driving::cli::context::Context;
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::theme;
use crate::infra::driving::cli::OutputFormat;

pub(in super::super) fn execute_source(
    matches: &ArgMatches,
    ctx: &Context<'_>,
    source_cfg: &SourceConfig,
) {
    match matches.subcommand() {
        Some(("list", _sub)) => execute_list(ctx, source_cfg),
        Some(("sync", sub)) => {
            let dry_run = sub.get_flag("dry-run");
            execute_sync(ctx, source_cfg, dry_run);
        }
        _ => {
            die1(
                CliError::new("unknown source subcommand").kind("validation"),
                ctx.output_fmt,
            );
        }
    }
}

fn resolve_token(source_cfg: &SourceConfig, output_fmt: OutputFormat) -> String {
    match std::env::var(&source_cfg.token_env) {
        Ok(t) if !t.is_empty() => t,
        _ => {
            die1(
                CliError::new(format!(
                    "environment variable '{}' is not set or empty",
                    source_cfg.token_env
                ))
                .kind("config"),
                output_fmt,
            );
        }
    }
}

fn make_gitlab_source<'a>(
    client: &'a crate::infra::driven::gitlab::UreqClient,
    source_cfg: &SourceConfig,
    token: &str,
    id_prefix: Option<&str>,
) -> crate::infra::driven::gitlab::GitLabSource<'a> {
    let mut source = crate::infra::driven::gitlab::GitLabSource::new(
        client,
        &source_cfg.url,
        &source_cfg.project,
        token,
    )
    .with_status_map(source_cfg.status_map.clone());
    if let Some(prefix) = id_prefix {
        source = source.with_id_prefix(prefix);
    }
    source
}

fn execute_list(ctx: &Context<'_>, source_cfg: &SourceConfig) {
    let token = resolve_token(source_cfg, ctx.output_fmt);
    let http_client = crate::infra::driven::gitlab::UreqClient;
    let source = make_gitlab_source(&http_client, source_cfg, &token, ctx.issues_id_prefix());

    match crate::domain::usecases::source::list_source_issues(&source) {
        Ok(issues) => {
            if issues.is_empty() {
                println!("No issues found in source '{}'.", source_cfg.name);
                return;
            }

            if ctx.output_fmt.is_structured() {
                use crate::infra::driving::cli::issue_view::IssueView;
                let views: Vec<IssueView> = issues.iter().map(IssueView::from_issue).collect();
                crate::infra::driving::cli::render_structured(&views, ctx.output_fmt);
                return;
            }

            use crate::infra::driving::cli::table::{terminal_width, Cell, Table};
            let mut table = Table::new(terminal_width());
            for issue in &issues {
                let cells = vec![
                    Cell::new(theme::id(&issue.id.to_string())),
                    Cell::new(theme::status(&issue.status.label, issue.status.category)),
                    Cell::new(issue.title.to_string()),
                ];
                table.push(cells);
            }
            table.print();
        }
        Err(e) => {
            die1(CliError::new(e.to_string()).kind("io"), ctx.output_fmt);
        }
    }
}

fn execute_sync(ctx: &Context<'_>, source_cfg: &SourceConfig, dry_run: bool) {
    let token = resolve_token(source_cfg, ctx.output_fmt);
    let http_client = crate::infra::driven::gitlab::UreqClient;
    let source = make_gitlab_source(&http_client, source_cfg, &token, ctx.issues_id_prefix());
    let repo = ctx.issue_repository();
    let id_gen = ctx.issue_id_generator();

    let options = crate::domain::usecases::source::SyncOptions { dry_run };
    if dry_run {
        println!("Dry run — no changes will be written.");
    }
    match crate::domain::usecases::source::sync_issues_with(&source, &repo, &id_gen, options) {
        Ok(result) => {
            for action in &result.actions {
                match action {
                    crate::domain::usecases::source::SyncAction::Created { id } => {
                        println!("  created  {id}");
                    }
                    crate::domain::usecases::source::SyncAction::Updated { id } => {
                        println!("  updated  {id}");
                    }
                    crate::domain::usecases::source::SyncAction::Unchanged { id } => {
                        if ctx.output_fmt.is_structured() {
                            println!("  unchanged  {id}");
                        }
                    }
                }
            }
            let verb = if dry_run { "Dry run" } else { "Sync" };
            println!(
                "{verb} complete: {} created, {} updated, {} unchanged.",
                result.created, result.updated, result.unchanged
            );
        }
        Err(e) => {
            die1(CliError::new(e.to_string()).kind("io"), ctx.output_fmt);
        }
    }
}