Skip to main content

yuki_cli/cli/
contacts.rs

1use crate::client::contact::{Contact, ContactClient};
2use crate::config::Config;
3use crate::error::YukiError;
4use crate::output::{
5    ListOptions, OutputFormat, apply_pagination, format_json, format_table, is_tty, select_fields,
6};
7
8fn contacts_to_rows(contacts: &[Contact]) -> Vec<Vec<String>> {
9    contacts
10        .iter()
11        .map(|c| {
12            vec![
13                c.id.clone(),
14                c.name.clone(),
15                c.contact_type.clone(),
16                c.country.clone(),
17                if c.is_supplier { "Yes" } else { "No" }.to_string(),
18                if c.is_customer { "Yes" } else { "No" }.to_string(),
19            ]
20        })
21        .collect()
22}
23
24pub async fn search(
25    config: &Config,
26    _admin: Option<&str>,
27    query: &str,
28    format: Option<&str>,
29) -> Result<(), YukiError> {
30    let mut client = ContactClient::new();
31    client.authenticate(&config.api_key).await?;
32    let contacts = client.search_contacts(query).await?;
33
34    let headers = vec![
35        "ID".into(),
36        "Name".into(),
37        "Type".into(),
38        "Country".into(),
39        "Supplier".into(),
40        "Customer".into(),
41    ];
42    let rows = contacts_to_rows(&contacts);
43
44    let fmt = OutputFormat::from_flag(format, is_tty());
45    match fmt {
46        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
47        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
48    }
49    Ok(())
50}
51
52/// The contact types Yuki's `ContactType` enum accepts.
53const CONTACT_TYPES: &[&str] = &["Customer", "Supplier", "Both", "None"];
54
55/// Normalise a contact type to the exact casing Yuki's schema requires.
56///
57/// Defaults to `Both`. An empty string is not a member of the enum, so passing one
58/// through makes the API reject the whole request with a schema validation fault.
59fn contact_type_value(requested: Option<&str>) -> Result<&'static str, YukiError> {
60    let Some(raw) = requested.map(str::trim).filter(|s| !s.is_empty()) else {
61        return Ok("Both");
62    };
63    CONTACT_TYPES
64        .iter()
65        .find(|valid| valid.eq_ignore_ascii_case(raw))
66        .copied()
67        .ok_or_else(|| {
68            YukiError::Config(format!(
69                "unknown contact type: {raw} (expected one of: {})",
70                CONTACT_TYPES.join(", ")
71            ))
72        })
73}
74
75pub async fn list(
76    config: &Config,
77    _admin: Option<&str>,
78    contact_type: Option<&str>,
79    format: Option<&str>,
80    opts: ListOptions<'_>,
81) -> Result<(), YukiError> {
82    let contact_type = contact_type_value(contact_type)?;
83    let mut client = ContactClient::new();
84    client.authenticate(&config.api_key).await?;
85    let contacts = client.get_suppliers_and_customers(contact_type).await?;
86
87    let mut headers = vec![
88        "ID".into(),
89        "Name".into(),
90        "Type".into(),
91        "Country".into(),
92        "Supplier".into(),
93        "Customer".into(),
94    ];
95    let mut rows = contacts_to_rows(&contacts);
96    apply_pagination(&mut rows, &opts);
97    select_fields(&mut headers, &mut rows, &opts)?;
98
99    let fmt = OutputFormat::from_flag(format, is_tty());
100    match fmt {
101        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
102        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
103    }
104    Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn defaults_to_both_when_unset() {
113        // Regression: `unwrap_or("")` sent an empty ContactType, which Yuki rejects
114        // with a schema validation fault, breaking `contacts list` entirely.
115        assert_eq!(contact_type_value(None).unwrap(), "Both");
116        assert_eq!(contact_type_value(Some("")).unwrap(), "Both");
117        assert_eq!(contact_type_value(Some("   ")).unwrap(), "Both");
118    }
119
120    #[test]
121    fn normalises_casing_to_the_schema() {
122        assert_eq!(contact_type_value(Some("customer")).unwrap(), "Customer");
123        assert_eq!(contact_type_value(Some("SUPPLIER")).unwrap(), "Supplier");
124        assert_eq!(contact_type_value(Some("Both")).unwrap(), "Both");
125        assert_eq!(contact_type_value(Some("none")).unwrap(), "None");
126    }
127
128    #[test]
129    fn rejects_values_outside_the_enum() {
130        let err = contact_type_value(Some("vendor")).unwrap_err();
131        assert!(err.to_string().contains("unknown contact type"));
132    }
133}