granger 0.1.0

An opinionated Kanban Board for the solo developer
Documentation
use crate::board;
use crate::common;
use crate::get_ticket_template_file;
use crate::state::model::State;
use crate::ticket;
use crate::ticket::model::Ticket;
use clap::Parser;
use std::env;
use std::fs;
use std::io::ErrorKind;

#[derive(Debug, Parser)]
pub struct TicketCli {
    /// Create a new Ticket
    #[arg(short, long)]
    create: bool,

    /// View details of an existing Ticket
    #[arg(short, long, value_name = "ticket number")]
    read: Option<usize>,

    /// Update details of an existing Ticket
    #[arg(short, long, value_name = "ticket number")]
    update: Option<usize>,

    /// Delete an existing Ticket
    #[arg(short, long, value_name = "ticket number")]
    delete: Option<usize>,

    /// List all existing Tickets in local Board
    #[arg(short, long)]
    list: bool,

    /// Move Ticket to a different State in local Board
    #[arg(short, long, num_args = 2, value_names = ["ticket number", "state"])]
    state: Option<Vec<String>>,
}

impl TicketCli {
    pub fn parse_options(self) {
        let board_id = common::get_current_board().id;

        if self.create {
            create_new_ticket();
        }

        if let Some(ticket_number) = self.read {
            read_ticket(board_id, ticket_number);
        }

        if let Some(ticket_number) = self.update {
            update_ticket(board_id, ticket_number);
        }

        if let Some(ticket_number) = self.delete {
            delete_ticket(board_id, ticket_number);
        }

        if self.list {
            list_all_tickets(board_id);
        }

        if let Some(passed_values) = self.state {
            let ticket_number: usize = passed_values[0].parse().unwrap();
            let state: &String = &passed_values[1];
            println!("{} - {}", ticket_number, state);
        }
    }
}

fn create_new_ticket() -> usize {
    open_interactive_ticket_file();

    let (title, description) = process_ticket_data();

    let board_id = common::get_current_board().id;

    let number = board::data::increment_and_get_ticket_count(board_id)
        .expect("Failed to get new incremented ticket count.");

    let ticket = Ticket::new(board_id, number, title, description);

    match ticket::data::add(&ticket) {
        Ok(_) => println!("'{}' Ticket created.", ticket.title),
        Err(value) => println!("Error creating '{}' Ticket : {}", ticket.title, value),
    }

    remove_ticket_file();

    number
}

fn read_ticket(board_id: usize, ticket_number: usize) {
    let ticket =
        ticket::data::get_by_number(board_id, ticket_number).expect("Failed to get Ticket.");

    println!("{:#?}", ticket);
}

fn update_ticket(board_id: usize, ticket_number: usize) {
    let ticket =
        ticket::data::get_by_number(board_id, ticket_number).expect("Failed to get Ticket.");

    let ticket_toml_string =
        toml::to_string(&ticket).expect("Failed to serialize Ticket to TOML string.");

    write_to_ticket_file(ticket_toml_string);

    open_interactive_ticket_file();

    let (title, description) = process_ticket_data();

    ticket::data::update(board_id, ticket_number, &title, &description)
        .expect("Failed to update Ticket.");
}

fn delete_ticket(board_id: usize, ticket_number: usize) {
    ticket::data::delete(board_id, ticket_number).expect("Failed to delete Ticket.");

    println!("Ticket number {} deleted successfully.", ticket_number);
}

fn list_all_tickets(board_id: usize) {
    let tickets =
        ticket::data::get_all_by_board_id(board_id).expect("Failed to retrieve all Tickets.");

    for ticket in tickets {
        if ticket.state == State::ToDo {
            println!("{} - {}", ticket.id, ticket.title);
        }
    }
}

fn open_interactive_ticket_file() {
    let editor = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());

    let temp_file = get_ticket_template_file();

    match std::process::Command::new(editor).arg(temp_file).status() {
        Ok(ticket_template) => ticket_template,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => panic!("Could not find editor provided."),
            other_error => panic!("{:?}", other_error),
        },
    };
}

fn process_ticket_data() -> (String, String) {
    let temp_file = get_ticket_template_file();

    let content = fs::read_to_string(temp_file)
        .expect("Failed to read content from new Ticket template file.");

    let deserialized_content: toml::Value = match toml::from_str(&content) {
        Ok(content) => content,
        Err(error) => panic!(
            "Error deserializing data from new ticket template: {}",
            error
        ),
    };

    let title = deserialized_content
        .get("title")
        .expect("Key: Title, not found in 'ticket_template.toml")
        .as_str()
        .expect("Failed to parse provided title as str.")
        .trim()
        .to_string();

    let description = deserialized_content
        .get("description")
        .expect("Key: Description, not found in 'ticket_template.toml")
        .as_str()
        .expect("Failed to parse provided description as str.")
        .trim()
        .to_string();

    (title, description)
}

fn remove_ticket_file() {
    let temp_file = get_ticket_template_file();

    std::process::Command::new("rm")
        .arg(temp_file)
        .status()
        .expect("Failed to remove temporary Ticket file.");
}

fn write_to_ticket_file(data: String) {
    let file = get_ticket_template_file();

    fs::write(file, data).expect("Failed to write to temporary Ticket file.");
}