Skip to main content

gor/
output.rs

1//! JSON and table formatting utilities for `gor`.
2//!
3//! Provides helpers for printing JSON output with optional field selection,
4//! date formatting, and number formatting. All output functions print to
5//! stdout and are intended for use by command implementations.
6
7#![allow(clippy::print_stdout)]
8
9use serde::Serialize;
10use serde_json::Value;
11
12/// Print a serializable value as pretty JSON to stdout.
13///
14/// If `fields` is `Some`, only the specified top-level fields are included
15/// in the output. If `fields` is `None`, the entire value is printed.
16///
17/// # Panics
18///
19/// Panics if the value cannot be serialized to JSON (this should never
20/// happen for well-formed data).
21///
22/// # Examples
23///
24/// ```no_run
25/// use gor::output::print_json;
26/// use serde_json::json;
27///
28/// let data = json!({"name": "hello-world", "stars": 42});
29/// print_json(&data, None::<&[String]>);
30/// ```
31#[allow(clippy::expect_used)]
32pub fn print_json<T: Serialize>(value: &T, fields: Option<&[String]>) {
33    let json_value = serde_json::to_value(value).expect("value must be serializable to JSON");
34
35    let output = match fields {
36        Some(field_list) => {
37            if field_list.is_empty() {
38                json_value
39            } else {
40                let mut filtered = serde_json::Map::new();
41                if let Value::Object(map) = &json_value {
42                    for field in field_list {
43                        if let Some(val) = map.get(field) {
44                            filtered.insert(field.clone(), val.clone());
45                        }
46                    }
47                }
48                Value::Object(filtered)
49            }
50        }
51        None => json_value,
52    };
53
54    let pretty = serde_json::to_string_pretty(&output).expect("value must be pretty-printable");
55    println!("{pretty}");
56}
57
58/// Format an ISO 8601 date string into a human-readable form.
59///
60/// Input format: `2024-01-15T10:30:00Z` or similar ISO 8601.
61/// Output format: `Jan 15, 2024`.
62///
63/// If the input cannot be parsed, the original string is returned.
64///
65/// # Examples
66///
67/// ```
68/// use gor::output::format_date;
69///
70/// assert_eq!(format_date("2024-01-15T10:30:00Z"), "Jan 15, 2024");
71/// assert_eq!(format_date("2023-12-25T00:00:00Z"), "Dec 25, 2023");
72/// ```
73#[must_use]
74pub fn format_date(iso_date: &str) -> String {
75    // Try to parse the date portion (first 10 characters: YYYY-MM-DD)
76    let date_part = iso_date.get(..10).unwrap_or(iso_date);
77
78    // Parse year, month, day
79    let parts: Vec<&str> = date_part.split('-').collect();
80    if parts.len() != 3 {
81        return iso_date.to_string();
82    }
83
84    let year = parts[0];
85    let month: u32 = parts[1].parse().unwrap_or(0);
86    let day = parts[2];
87
88    let month_name = match month {
89        1 => "Jan",
90        2 => "Feb",
91        3 => "Mar",
92        4 => "Apr",
93        5 => "May",
94        6 => "Jun",
95        7 => "Jul",
96        8 => "Aug",
97        9 => "Sep",
98        10 => "Oct",
99        11 => "Nov",
100        12 => "Dec",
101        _ => return iso_date.to_string(),
102    };
103
104    // Strip leading zeros from day
105    let day_stripped = day.trim_start_matches('0');
106
107    format!("{month_name} {day_stripped}, {year}")
108}
109
110/// Format a number with comma separators for thousands.
111///
112/// # Examples
113///
114/// ```
115/// use gor::output::format_count;
116///
117/// assert_eq!(format_count(0), "0");
118/// assert_eq!(format_count(42), "42");
119/// assert_eq!(format_count(1_234), "1,234");
120/// assert_eq!(format_count(1_000_000), "1,000,000");
121/// ```
122#[must_use]
123pub fn format_count(n: u64) -> String {
124    let s = n.to_string();
125    let mut result = String::with_capacity(s.len() + s.len() / 3);
126
127    for (i, c) in s.chars().enumerate() {
128        // Insert a comma every 3 digits from the right
129        if i > 0 && (s.len() - i) % 3 == 0 {
130            result.push(',');
131        }
132        result.push(c);
133    }
134
135    result
136}
137
138#[cfg(test)]
139#[allow(clippy::expect_used)]
140mod tests {
141    use super::*;
142    use serde_json::json;
143
144    #[test]
145    fn print_json_full_output() {
146        let data = json!({"name": "hello-world", "stars": 42, "forks": 7});
147        // Just verify it doesn't panic
148        print_json(&data, None::<&[String]>);
149    }
150
151    #[test]
152    fn print_json_with_fields() {
153        let data = json!({"name": "hello-world", "stars": 42, "forks": 7});
154        let fields = vec!["name".to_string(), "stars".to_string()];
155        // Just verify it doesn't panic
156        print_json(&data, Some(&fields));
157    }
158
159    #[test]
160    fn print_json_with_empty_fields() {
161        let data = json!({"name": "hello-world", "stars": 42});
162        let fields: Vec<String> = vec![];
163        // Empty fields list should print everything
164        print_json(&data, Some(&fields));
165    }
166
167    #[test]
168    fn print_json_with_missing_field() {
169        let data = json!({"name": "hello-world"});
170        let fields = vec!["name".to_string(), "missing".to_string()];
171        // Missing fields are silently omitted
172        print_json(&data, Some(&fields));
173    }
174
175    #[test]
176    fn format_date_standard() {
177        assert_eq!(format_date("2024-01-15T10:30:00Z"), "Jan 15, 2024");
178    }
179
180    #[test]
181    fn format_date_december() {
182        assert_eq!(format_date("2023-12-25T00:00:00Z"), "Dec 25, 2023");
183    }
184
185    #[test]
186    fn format_date_single_digit_day() {
187        assert_eq!(format_date("2024-03-05T12:00:00Z"), "Mar 5, 2024");
188    }
189
190    #[test]
191    fn format_date_invalid() {
192        assert_eq!(format_date("not-a-date"), "not-a-date");
193    }
194
195    #[test]
196    fn format_date_empty() {
197        assert_eq!(format_date(""), "");
198    }
199
200    #[test]
201    fn format_date_bad_month() {
202        assert_eq!(format_date("2024-13-01"), "2024-13-01");
203    }
204
205    #[test]
206    fn format_count_zero() {
207        assert_eq!(format_count(0), "0");
208    }
209
210    #[test]
211    fn format_count_small() {
212        assert_eq!(format_count(42), "42");
213    }
214
215    #[test]
216    fn format_count_thousands() {
217        assert_eq!(format_count(1_234), "1,234");
218    }
219
220    #[test]
221    fn format_count_millions() {
222        assert_eq!(format_count(1_000_000), "1,000,000");
223    }
224
225    #[test]
226    fn format_count_billions() {
227        assert_eq!(format_count(1_234_567_890), "1,234,567,890");
228    }
229
230    #[test]
231    fn format_count_ten_thousands() {
232        assert_eq!(format_count(10_000), "10,000");
233    }
234
235    #[test]
236    fn format_count_hundred_thousands() {
237        assert_eq!(format_count(100_000), "100,000");
238    }
239}