toggl 0.5.0

Unofficial command-line interface for Toggl Track using the v9 API.
use crate::api;
use crate::arguments::Entity;
use crate::models;
use api::client::ApiClient;
use colored::Colorize;
use models::ResultWithDefaultError;
use std::io::{self, BufWriter, Write};

pub struct ListCommand;

impl ListCommand {
    pub async fn execute(
        api_client: impl ApiClient,
        count: Option<usize>,
        json_flag: bool,
        since: Option<String>,
        until: Option<String>,
        entity: Option<Entity>,
    ) -> ResultWithDefaultError<()> {
        if let Some(Entity::Tag { json: entity_json }) = entity {
            let json = json_flag || entity_json;
            let user = api_client.get_user().await?;
            match api_client.get_tags(user.default_workspace_id).await {
                Err(error) => println!("{}\n{}", "Couldn't fetch tags from API".red(), error),
                Ok(tags) => {
                    let stdout = io::stdout();
                    let mut handle = BufWriter::new(stdout);
                    let tags = tags
                        .iter()
                        .take(count.unwrap_or(usize::MAX))
                        .collect::<Vec<_>>();
                    if json {
                        let json_string = serde_json::to_string_pretty(&tags)
                            .expect("failed to serialize tags to JSON");
                        writeln!(handle, "{json_string}").expect("failed to print");
                    } else {
                        tags.iter()
                            .for_each(|tag| writeln!(handle, "{tag}").expect("failed to print"));
                    }
                }
            }
            return Ok(());
        }

        let is_time_entry = matches!(entity, None | Some(Entity::TimeEntry { .. }));
        let has_date_filter = since.is_some() || until.is_some();

        if is_time_entry && has_date_filter {
            let stdout = io::stdout();
            let mut handle = BufWriter::new(stdout);
            let json = match &entity {
                Some(Entity::TimeEntry { json }) => json_flag || *json,
                _ => json_flag,
            };
            match api_client.get_time_entries_filtered(since, until).await {
                Err(error) => println!(
                    "{}\n{}",
                    "Couldn't fetch time entries from API".red(),
                    error
                ),
                Ok(entries) => {
                    let entries = entries
                        .iter()
                        .take(count.unwrap_or(usize::MAX))
                        .collect::<Vec<_>>();
                    if json {
                        let json_string = serde_json::to_string_pretty(&entries)
                            .expect("failed to serialize time entries to JSON");
                        writeln!(handle, "{json_string}").expect("failed to print");
                    } else {
                        entries
                            .iter()
                            .for_each(|te| writeln!(handle, "{te}").expect("failed to print"));
                    }
                }
            }
            return Ok(());
        }

        match api_client.get_entities().await {
            Err(error) => println!(
                "{}\n{}",
                "Couldn't fetch time entries the from API".red(),
                error
            ),
            Ok(entities) => {
                // use this to avoid calling println! in a loop:
                // <https://rust-cli.github.io/book/tutorial/output.html#a-note-on-printing-performance>
                let stdout = io::stdout();
                let mut handle = BufWriter::new(stdout);

                // TODO: better error handling for writeln!
                match entity.unwrap_or(Entity::TimeEntry { json: false }) {
                    Entity::TimeEntry { json: entity_json } => {
                        let json = json_flag || entity_json;
                        let entries = entities
                            .time_entries
                            .iter()
                            .take(count.unwrap_or(usize::MAX))
                            .collect::<Vec<_>>();

                        if json {
                            let json_string = serde_json::to_string_pretty(&entries)
                                .expect("failed to serialize time entries to JSON");
                            writeln!(handle, "{json_string}").expect("failed to print");
                        } else {
                            entries.iter().for_each(|time_entry| {
                                writeln!(handle, "{time_entry}").expect("failed to print")
                            });
                        }
                    }

                    Entity::Project { json: entity_json } => {
                        let json = json_flag || entity_json;
                        let projects = entities
                            .projects
                            .values()
                            .take(count.unwrap_or(usize::MAX))
                            .collect::<Vec<_>>();

                        if json {
                            let json_string = serde_json::to_string_pretty(&projects)
                                .expect("failed to serialize projects to JSON");
                            writeln!(handle, "{json_string}").expect("failed to print");
                        } else {
                            projects.iter().for_each(|project| {
                                writeln!(handle, "{project}").expect("failed to print")
                            });
                        }
                    }

                    // Already handled above, but needed for exhaustive match
                    Entity::Tag { .. } => unreachable!(),
                };
            }
        }
        Ok(())
    }
}