tftio-asana-cli 3.1.0

An interface to the Asana API
Documentation
//! Section CLI command implementations.

use super::build_api_client;
use crate::{
    api,
    config::Config,
    models::{SectionCreateData, SectionCreateRequest, SectionUpdateData, SectionUpdateRequest},
    output::OutputFormat,
};
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use dialoguer::Confirm;
use std::io::{IsTerminal, stdout};
use tokio::runtime::Builder as RuntimeBuilder;

#[derive(Subcommand, Debug)]
pub enum SectionCommand {
    /// List sections in a project.
    List(SectionListArgs),
    /// Display detailed information about a section.
    Show(SectionShowArgs),
    /// Create a new section in a project.
    Create(SectionCreateArgs),
    /// Update an existing section.
    Update(SectionUpdateArgs),
    /// Delete a section.
    Delete(SectionDeleteArgs),
    /// Operate on tasks within a section.
    Tasks {
        #[command(subcommand)]
        command: SectionTasksCommand,
    },
}

/// Subcommands for `section <SECTION> tasks`.
#[derive(Subcommand, Debug)]
pub enum SectionTasksCommand {
    /// List tasks in the section.
    List(SectionTasksArgs),
}

#[derive(Args, Debug)]
pub struct SectionListArgs {
    /// Project identifier (gid) to list sections from.
    #[arg(long)]
    pub project: String,
    /// Output format override.
    #[arg(long, value_enum)]
    pub output: Option<OutputFormat>,
}

#[derive(Args, Debug)]
pub struct SectionShowArgs {
    /// Section identifier (gid).
    #[arg(value_name = "SECTION")]
    pub section: String,
    /// Additional fields to request from the API.
    #[arg(long, value_name = "FIELD")]
    pub fields: Vec<String>,
    /// Output format override.
    #[arg(long, value_enum)]
    pub output: Option<OutputFormat>,
}

#[derive(Args, Debug)]
pub struct SectionCreateArgs {
    /// Section name.
    #[arg(long)]
    pub name: String,
    /// Project identifier (gid) to create the section in.
    #[arg(long)]
    pub project: String,
    /// Insert before this section gid.
    #[arg(long = "insert-before")]
    pub insert_before: Option<String>,
    /// Insert after this section gid.
    #[arg(long = "insert-after")]
    pub insert_after: Option<String>,
    /// Output format override.
    #[arg(long, value_enum)]
    pub output: Option<OutputFormat>,
}

#[derive(Args, Debug)]
pub struct SectionUpdateArgs {
    /// Section identifier (gid).
    #[arg(value_name = "SECTION")]
    pub section: String,
    /// New section name.
    #[arg(long)]
    pub name: Option<String>,
    /// Output format override.
    #[arg(long, value_enum)]
    pub output: Option<OutputFormat>,
}

#[derive(Args, Debug)]
pub struct SectionDeleteArgs {
    /// Section identifier (gid).
    #[arg(value_name = "SECTION")]
    pub section: String,
    /// Skip confirmation prompt.
    #[arg(long)]
    pub force: bool,
}

#[derive(Args, Debug)]
pub struct SectionTasksArgs {
    /// Section identifier (gid).
    #[arg(value_name = "SECTION")]
    pub section: String,
    /// Additional fields to request from the API.
    #[arg(long, value_name = "FIELD")]
    pub fields: Vec<String>,
    /// Output format override.
    #[arg(long, value_enum)]
    pub output: Option<OutputFormat>,
}

pub fn execute_section_command(cmd: SectionCommand, config: &Config) -> Result<()> {
    let runtime = RuntimeBuilder::new_current_thread()
        .enable_all()
        .build()
        .context("failed to build tokio runtime")?;

    runtime.block_on(async { execute_section_command_async(cmd, config).await })
}

async fn execute_section_command_async(cmd: SectionCommand, config: &Config) -> Result<()> {
    match cmd {
        SectionCommand::List(args) => list_sections(args, config).await,
        SectionCommand::Show(args) => show_section(args, config).await,
        SectionCommand::Create(args) => create_section(args, config).await,
        SectionCommand::Update(args) => update_section(args, config).await,
        SectionCommand::Delete(args) => delete_section(args, config).await,
        SectionCommand::Tasks { command } => match command {
            SectionTasksCommand::List(args) => list_section_tasks(args, config).await,
        },
    }
}

fn determine_output(value: Option<OutputFormat>) -> OutputFormat {
    value.unwrap_or_else(|| {
        if stdout().is_terminal() {
            OutputFormat::Table
        } else {
            OutputFormat::Json
        }
    })
}

async fn list_sections(args: SectionListArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;
    let sections = api::list_sections(&client, &args.project).await?;
    let format = determine_output(args.output);

    match format {
        OutputFormat::Json => {
            let json = serde_json::to_string_pretty(&sections)?;
            println!("{json}");
        }
        OutputFormat::Csv => {
            println!("gid,name,project_gid,project_name");
            for section in sections {
                let project_gid = section.project.as_ref().map_or("", |p| p.gid.as_str());
                let project_name = section
                    .project
                    .as_ref()
                    .and_then(|p| p.name.as_deref())
                    .unwrap_or("");
                println!(
                    "{},{},{},{}",
                    section.gid, section.name, project_gid, project_name
                );
            }
        }
        OutputFormat::Markdown => {
            println!("| GID | Name | Project |");
            println!("|---|---|---|");
            for section in sections {
                let project_label = section.project.as_ref().map_or_else(
                    || "N/A".to_string(),
                    super::super::models::section::SectionProjectReference::label,
                );
                println!("| {} | {} | {} |", section.gid, section.name, project_label);
            }
        }
        OutputFormat::Table => {
            if sections.is_empty() {
                println!("No sections found in project.");
            } else {
                println!("{:<20} {:<30} {:<20}", "GID", "NAME", "PROJECT");
                println!("{}", "-".repeat(72));
                for section in sections {
                    let project_label = section.project.as_ref().map_or_else(
                        || "N/A".to_string(),
                        super::super::models::section::SectionProjectReference::label,
                    );
                    println!(
                        "{:<20} {:<30} {:<20}",
                        section.gid, section.name, project_label
                    );
                }
            }
        }
    }

    Ok(())
}

fn render_section_detail(section: &crate::models::Section) {
    println!("Section: {}", section.name);
    println!("GID: {}", section.gid);
    if let Some(project) = &section.project {
        println!(
            "Project: {} ({})",
            project.name.as_deref().unwrap_or("N/A"),
            project.gid
        );
    }
    if let Some(created_at) = &section.created_at {
        println!("Created: {created_at}");
    }
}

async fn show_section(args: SectionShowArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;
    let section = api::get_section(&client, &args.section, args.fields).await?;
    let format = determine_output(args.output);

    if matches!(format, OutputFormat::Json) {
        let json = serde_json::to_string_pretty(&section)?;
        println!("{json}");
    } else {
        render_section_detail(&section);
    }

    Ok(())
}

async fn create_section(args: SectionCreateArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;

    let request = SectionCreateRequest {
        data: SectionCreateData {
            name: args.name,
            insert_before: args.insert_before,
            insert_after: args.insert_after,
        },
    };

    let section = api::create_section(&client, &args.project, request).await?;
    let format = determine_output(args.output);

    if matches!(format, OutputFormat::Json) {
        let json = serde_json::to_string_pretty(&section)?;
        println!("{json}");
    } else {
        println!("Created section: {}", section.name);
        println!("GID: {}", section.gid);
        if let Some(project) = &section.project {
            println!(
                "Project: {} ({})",
                project.name.as_deref().unwrap_or("N/A"),
                project.gid
            );
        }
    }

    Ok(())
}

async fn update_section(args: SectionUpdateArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;

    if args.name.is_none() {
        bail!("no updates specified; supply --name");
    }

    let request = SectionUpdateRequest {
        data: SectionUpdateData { name: args.name },
    };

    let section = api::update_section(&client, &args.section, request).await?;
    let format = determine_output(args.output);

    if matches!(format, OutputFormat::Json) {
        let json = serde_json::to_string_pretty(&section)?;
        println!("{json}");
    } else {
        println!("Updated section {}.", section.gid);
        render_section_detail(&section);
    }

    Ok(())
}

async fn delete_section(args: SectionDeleteArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;

    if !args.force {
        let confirmed = Confirm::new()
            .with_prompt(format!("Delete section {}?", args.section))
            .default(false)
            .interact()
            .context("failed to read confirmation")?;
        if !confirmed {
            println!("Deletion aborted.");
            return Ok(());
        }
    }

    api::delete_section(&client, &args.section).await?;
    println!("Deleted section {}.", args.section);
    Ok(())
}

async fn list_section_tasks(args: SectionTasksArgs, config: &Config) -> Result<()> {
    let client = build_api_client(config)?;
    let tasks = api::get_section_tasks(&client, &args.section, args.fields).await?;
    let format = determine_output(args.output);

    match format {
        OutputFormat::Json => {
            let json = serde_json::to_string_pretty(&tasks)?;
            println!("{json}");
        }
        OutputFormat::Csv => {
            println!("gid,name,completed,assignee");
            for task in tasks {
                let assignee = task.assignee.as_ref().map_or_else(
                    || "Unassigned".to_string(),
                    super::super::models::user::UserReference::label,
                );
                println!("{},{},{},{}", task.gid, task.name, task.completed, assignee);
            }
        }
        OutputFormat::Markdown => {
            println!("| GID | Name | Status | Assignee |");
            println!("|---|---|---|---|");
            for task in tasks {
                let status = if task.completed { "Done" } else { "Open" };
                let assignee = task.assignee.as_ref().map_or_else(
                    || "Unassigned".to_string(),
                    super::super::models::user::UserReference::label,
                );
                println!(
                    "| {} | {} | {} | {} |",
                    task.gid, task.name, status, assignee
                );
            }
        }
        OutputFormat::Table => {
            if tasks.is_empty() {
                println!("No tasks found in section.");
            } else {
                println!(
                    "{:<20} {:<40} {:<10} {:<20}",
                    "GID", "NAME", "STATUS", "ASSIGNEE"
                );
                println!("{}", "-".repeat(92));
                for task in tasks {
                    let status = if task.completed { "Done" } else { "Open" };
                    let assignee = task.assignee.as_ref().map_or_else(
                        || "Unassigned".to_string(),
                        super::super::models::user::UserReference::label,
                    );
                    println!(
                        "{:<20} {:<40} {:<10} {:<20}",
                        task.gid, task.name, status, assignee
                    );
                }
            }
        }
    }

    Ok(())
}