bzr 0.1.0

A CLI for Bugzilla, inspired by gh
Documentation
use serde::Serialize;
use tabled::{Table, Tabled};

use super::formatting::{print_formatted, yes_no};
use crate::types::{FieldValue, OutputFormat};

#[derive(Tabled)]
struct FieldValueRow {
    #[tabled(rename = "NAME")]
    name: String,
    #[tabled(rename = "ACTIVE")]
    active: String,
    #[tabled(rename = "CAN CHANGE TO")]
    can_change_to: String,
}

#[expect(clippy::print_stdout)]
pub fn print_field_values(values: &[FieldValue], format: OutputFormat) {
    print_formatted(values, format, |values| {
        let rows: Vec<FieldValueRow> = values
            .iter()
            .map(|v| {
                let transitions = v
                    .can_change_to
                    .as_ref()
                    .map(|t| {
                        t.iter()
                            .map(|s| s.name.as_str())
                            .collect::<Vec<_>>()
                            .join(", ")
                    })
                    .unwrap_or_default();
                FieldValueRow {
                    name: v.name.clone(),
                    active: yes_no(v.is_active).into(),
                    can_change_to: transitions,
                }
            })
            .collect();
        println!("{}", Table::new(rows));
    });
}

#[derive(Serialize, Tabled)]
struct FieldAliasRow {
    #[tabled(rename = "ALIAS")]
    alias: &'static str,
    #[tabled(rename = "API FIELD NAME")]
    api_name: &'static str,
}

#[expect(clippy::print_stdout)]
pub fn print_field_aliases(aliases: &[(&'static str, &'static str)], format: OutputFormat) {
    let rows: Vec<FieldAliasRow> = aliases
        .iter()
        .map(|&(alias, api_name)| FieldAliasRow { alias, api_name })
        .collect();
    print_formatted(&rows, format, |rows| {
        println!("{}", Table::new(rows));
    });
}

#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
    use crate::types::{FieldValue, StatusTransition};

    #[test]
    fn print_field_values_json_empty() {
        let values: Vec<FieldValue> = vec![];
        let json = serde_json::to_string_pretty(&values).unwrap();
        assert_eq!(json, "[]");
    }

    #[test]
    fn print_field_values_json_with_transitions() {
        let values = vec![FieldValue {
            name: "NEW".into(),
            sort_key: 0,
            is_active: true,
            can_change_to: Some(vec![
                StatusTransition {
                    name: "ASSIGNED".into(),
                },
                StatusTransition {
                    name: "RESOLVED".into(),
                },
            ]),
        }];
        let json = serde_json::to_string_pretty(&values).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed[0]["name"], "NEW");
        assert_eq!(parsed[0]["is_active"], true);
        let transitions = parsed[0]["can_change_to"].as_array().unwrap();
        assert_eq!(transitions.len(), 2);
        assert_eq!(transitions[0]["name"], "ASSIGNED");
    }

    #[test]
    fn print_field_values_table_renders_rows() {
        use super::FieldValueRow;
        use tabled::Table;

        let rows = vec![
            FieldValueRow {
                name: "NEW".into(),
                active: "Yes".into(),
                can_change_to: "ASSIGNED, RESOLVED".into(),
            },
            FieldValueRow {
                name: "CLOSED".into(),
                active: "No".into(),
                can_change_to: String::new(),
            },
        ];
        let table = Table::new(rows).to_string();
        assert!(table.contains("NEW"));
        assert!(table.contains("CLOSED"));
        assert!(table.contains("Yes"));
        assert!(table.contains("No"));
        assert!(table.contains("ASSIGNED, RESOLVED"));
        assert!(table.contains("NAME"));
        assert!(table.contains("ACTIVE"));
        assert!(table.contains("CAN CHANGE TO"));
    }

    #[test]
    fn print_field_values_json_active_and_inactive() {
        let values = vec![
            FieldValue {
                name: "NEW".into(),
                sort_key: 0,
                is_active: true,
                can_change_to: Some(vec![StatusTransition {
                    name: "ASSIGNED".into(),
                }]),
            },
            FieldValue {
                name: "CLOSED".into(),
                sort_key: 1,
                is_active: false,
                can_change_to: None,
            },
        ];
        let json = serde_json::to_string_pretty(&values).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed[0]["name"], "NEW");
        assert_eq!(parsed[0]["is_active"], true);
        assert_eq!(parsed[1]["name"], "CLOSED");
        assert_eq!(parsed[1]["is_active"], false);
        assert!(parsed[1]["can_change_to"].is_null());
    }

    #[test]
    fn print_field_aliases_json_serialization() {
        let aliases: &[(&str, &str)] = &[("status", "bug_status"), ("severity", "bug_severity")];
        let rows: Vec<super::FieldAliasRow> = aliases
            .iter()
            .map(|&(alias, api_name)| super::FieldAliasRow { alias, api_name })
            .collect();
        let json = serde_json::to_string_pretty(&rows).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        let arr = parsed.as_array().unwrap();
        assert_eq!(arr.len(), 2);
        assert_eq!(arr[0]["alias"], "status");
        assert_eq!(arr[0]["api_name"], "bug_status");
        assert_eq!(arr[1]["alias"], "severity");
        assert_eq!(arr[1]["api_name"], "bug_severity");
    }
}