1use std::collections::{HashMap, HashSet};
35use std::io::{self, Write};
36
37use comfy_table::presets::UTF8_FULL_CONDENSED;
38use comfy_table::{ContentArrangement, Table};
39use serde::Serialize;
40use serde_json::Map;
41
42const MAX_TABLE_COLUMNS: usize = 12;
44
45const MAX_CELL_LENGTH: usize = 60;
47
48const CELL_TRUNCATE_AT: usize = MAX_CELL_LENGTH - 3;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum OutputFormat {
54 Json,
56 Table,
58 Csv,
60}
61
62pub fn format_and_print<T: Serialize>(
74 data: &T,
75 format: OutputFormat,
76 fields: Option<&[String]>,
77 truncate: Option<usize>,
78 writer: &mut impl Write,
79) -> io::Result<()> {
80 let mut value = serde_json::to_value(data).map_err(io::Error::other)?;
81
82 if let Some(fields) = fields {
83 project_fields(&mut value, fields);
84 }
85 if let Some(max_chars) = truncate {
86 let count = truncate_values(&mut value, max_chars);
87 if count > 0 {
88 inject_truncated_count(&mut value, count);
89 }
90 }
91
92 match format {
93 OutputFormat::Json => print_json(&value, writer),
94 OutputFormat::Table => print_table(&value, writer),
95 OutputFormat::Csv => print_csv(&value, writer),
96 }
97}
98
99fn print_json(value: &serde_json::Value, writer: &mut impl Write) -> io::Result<()> {
103 serde_json::to_writer_pretty(&mut *writer, value).map_err(io::Error::other)?;
104 writeln!(writer)
105}
106
107fn print_table(value: &serde_json::Value, writer: &mut impl Write) -> io::Result<()> {
114 let rows = extract_rows(value);
115
116 if rows.is_empty() {
117 return writeln!(writer, "No results found.");
118 }
119
120 let (flat_rows, headers) = flatten_and_collect(&rows);
121
122 let priority = [
124 "name",
125 "timestamp",
126 "message",
127 "severity",
128 "service",
129 "source",
130 "status",
131 "trace_id",
132 "span_id",
133 "labels",
134 "points",
135 "stats",
136 "result_type",
137 "type",
138 "description",
139 "unit",
140 ];
141 let final_headers = prioritize_headers(&headers, &priority, MAX_TABLE_COLUMNS);
142
143 let mut table = Table::new();
144 table
145 .load_preset(UTF8_FULL_CONDENSED)
146 .set_content_arrangement(ContentArrangement::Dynamic)
147 .set_header(&final_headers);
148
149 for row in &flat_rows {
150 let cells: Vec<String> = final_headers
151 .iter()
152 .map(|h| format_cell(row.get(h.as_str())))
153 .collect();
154 table.add_row(cells);
155 }
156
157 writeln!(writer, "{table}")?;
158
159 if flat_rows.len() > 1 {
161 writeln!(writer, "\n({} rows)", flat_rows.len())?;
162 }
163
164 Ok(())
165}
166
167fn prioritize_headers(all: &[String], priority: &[&str], max_cols: usize) -> Vec<String> {
169 let all_set: HashSet<&str> = all.iter().map(String::as_str).collect();
170 let mut result: Vec<String> = Vec::new();
171
172 for &p in priority {
174 if all_set.contains(p) && result.len() < max_cols {
175 result.push(p.to_string());
176 }
177 }
178
179 for h in all {
181 if result.len() >= max_cols {
182 break;
183 }
184 if !result.contains(h) {
185 result.push(h.clone());
186 }
187 }
188
189 result
190}
191
192fn print_csv(value: &serde_json::Value, writer: &mut impl Write) -> io::Result<()> {
196 let rows = extract_rows(value);
197 if rows.is_empty() {
198 return Ok(());
199 }
200
201 let (flat_rows, headers) = flatten_and_collect(&rows);
202
203 let mut wtr = csv::Writer::from_writer(&mut *writer);
204 wtr.write_record(&headers).map_err(io::Error::other)?;
205
206 for row in &flat_rows {
207 let cells: Vec<String> = headers
208 .iter()
209 .map(|h| csv_cell(row.get(h.as_str())))
210 .collect();
211 wtr.write_record(&cells).map_err(io::Error::other)?;
212 }
213
214 wtr.flush()?;
215 Ok(())
216}
217
218fn csv_cell(value: Option<&serde_json::Value>) -> String {
220 match value {
221 None | Some(serde_json::Value::Null) => String::new(),
222 Some(serde_json::Value::String(s)) => s.clone(),
223 Some(serde_json::Value::Number(n)) => n.to_string(),
224 Some(serde_json::Value::Bool(b)) => b.to_string(),
225 Some(other) => other.to_string(),
226 }
227}
228
229fn project_fields(value: &mut serde_json::Value, fields: &[String]) {
240 let selectors = build_field_selectors(fields);
241 if selectors.is_empty() {
242 return;
243 }
244
245 project_in_value(value, &selectors);
246}
247
248fn project_in_value(
249 value: &mut serde_json::Value,
250 selectors: &HashMap<String, Option<HashSet<String>>>,
251) {
252 match value {
253 serde_json::Value::Array(arr) => {
255 for item in arr {
256 if item.is_object() {
257 project_row_fields(item, selectors);
258 }
259 }
260 }
261 serde_json::Value::Object(map) => {
263 if let Some(data) = map.get_mut("data") {
264 project_in_data_section(data, selectors);
265 }
266 }
267 _ => {}
268 }
269}
270
271fn project_in_data_section(
272 data: &mut serde_json::Value,
273 selectors: &HashMap<String, Option<HashSet<String>>>,
274) {
275 match data {
276 serde_json::Value::Array(arr) => {
278 for item in arr {
279 if item.is_object() {
280 project_row_fields(item, selectors);
281 }
282 }
283 }
284 serde_json::Value::Object(map) => {
286 for field_value in map.values_mut() {
287 if let serde_json::Value::Array(arr) = field_value {
288 let has_objects = arr.iter().any(serde_json::Value::is_object);
289 if has_objects {
290 for item in arr {
291 if item.is_object() {
292 project_row_fields(item, selectors);
293 }
294 }
295 }
296 }
297 }
298 }
299 _ => {}
300 }
301}
302
303fn truncate_values(value: &mut serde_json::Value, max_chars: usize) -> usize {
312 truncate_in_value(value, max_chars)
313}
314
315fn truncate_in_value(value: &mut serde_json::Value, max_chars: usize) -> usize {
316 match value {
317 serde_json::Value::Array(arr) => arr
318 .iter_mut()
319 .map(|item| truncate_value_recursive(item, max_chars))
320 .sum(),
321 serde_json::Value::Object(map) => {
322 if let Some(data) = map.get_mut("data") {
323 truncate_in_data_section(data, max_chars)
324 } else {
325 0
326 }
327 }
328 _ => 0,
329 }
330}
331
332fn truncate_in_data_section(data: &mut serde_json::Value, max_chars: usize) -> usize {
333 match data {
334 serde_json::Value::Array(arr) => arr
335 .iter_mut()
336 .map(|item| truncate_value_recursive(item, max_chars))
337 .sum(),
338 serde_json::Value::Object(map) => {
339 let mut count = 0;
340 for field_value in map.values_mut() {
341 if let serde_json::Value::Array(arr) = field_value {
342 let has_objects = arr.iter().any(serde_json::Value::is_object);
343 if has_objects {
344 for item in arr {
345 count += truncate_value_recursive(item, max_chars);
346 }
347 }
348 }
349 }
350 count
351 }
352 _ => 0,
353 }
354}
355
356fn inject_truncated_count(value: &mut serde_json::Value, count: usize) {
357 if let serde_json::Value::Object(map) = value {
358 if let Some(serde_json::Value::Object(meta)) = map.get_mut("metadata") {
359 meta.insert(
360 "truncated_values".to_string(),
361 serde_json::Value::Number(count.into()),
362 );
363 }
364 }
365}
366
367fn extract_rows(value: &serde_json::Value) -> Vec<&serde_json::Value> {
377 match value {
378 serde_json::Value::Array(arr) => arr.iter().collect(),
379 serde_json::Value::Object(map) => {
380 if let Some(data) = map.get("data") {
383 if let serde_json::Value::Object(data_map) = data {
384 if let Some(series) = data_map.get("series") {
386 return extract_rows(series);
387 }
388 if let Some(entries) = data_map.get("entries") {
390 return extract_rows(entries);
391 }
392 if let Some(spans) = data_map.get("spans") {
394 return extract_rows(spans);
395 }
396 if let Some(items) = data_map.get("items") {
398 return extract_rows(items);
399 }
400 if let Some(ext_data) = data_map.get("data") {
402 return extract_rows(ext_data);
403 }
404 if let Some(scalar) = data_map.get("scalar") {
407 return vec![scalar];
409 }
410 if let Some(info) = data_map.get("info") {
412 if !info.is_null() {
413 return vec![info];
414 }
415 return vec![];
416 }
417 }
418 return extract_rows(data);
420 }
421 vec![value]
423 }
424 _ => vec![],
425 }
426}
427
428fn build_field_selectors(fields: &[String]) -> HashMap<String, Option<HashSet<String>>> {
429 let mut selectors: HashMap<String, Option<HashSet<String>>> = HashMap::new();
430
431 for field in fields {
432 let field = field.trim();
433 if field.is_empty() {
434 continue;
435 }
436
437 if let Some((top, nested)) = field.split_once('.') {
438 if top.is_empty() || nested.is_empty() {
439 continue;
440 }
441 match selectors.get_mut(top) {
442 Some(None) => {}
443 Some(Some(nested_fields)) => {
444 nested_fields.insert(nested.to_string());
445 }
446 None => {
447 let mut nested_fields = HashSet::new();
448 nested_fields.insert(nested.to_string());
449 selectors.insert(top.to_string(), Some(nested_fields));
450 }
451 }
452 } else {
453 selectors.insert(field.to_string(), None);
454 }
455 }
456
457 selectors
458}
459
460fn project_row_fields(
461 row: &mut serde_json::Value,
462 selectors: &HashMap<String, Option<HashSet<String>>>,
463) {
464 let serde_json::Value::Object(original) = row else {
465 return;
466 };
467
468 let taken = std::mem::take(original);
469 let mut projected = Map::new();
470
471 for (key, value) in taken {
472 match selectors.get(&key) {
473 Some(None) => {
474 projected.insert(key, value);
475 }
476 Some(Some(nested_fields)) => {
477 if let Some(nested_value) = project_nested_value(&value, nested_fields) {
478 projected.insert(key, nested_value);
479 }
480 }
481 None => {}
482 }
483 }
484
485 *original = projected;
486}
487
488fn project_nested_value(
489 value: &serde_json::Value,
490 nested_fields: &HashSet<String>,
491) -> Option<serde_json::Value> {
492 let serde_json::Value::Object(map) = value else {
493 return None;
494 };
495
496 let mut projected = Map::new();
497 for (key, nested_value) in map {
498 if nested_fields.contains(key) {
499 projected.insert(key.clone(), nested_value.clone());
500 }
501 }
502
503 if projected.is_empty() {
504 None
505 } else {
506 Some(serde_json::Value::Object(projected))
507 }
508}
509
510fn truncate_value_recursive(value: &mut serde_json::Value, max_chars: usize) -> usize {
511 match value {
512 serde_json::Value::String(s) => {
513 if truncate_string_in_place(s, max_chars) {
514 1
515 } else {
516 0
517 }
518 }
519 serde_json::Value::Array(arr) => arr
520 .iter_mut()
521 .map(|item| truncate_value_recursive(item, max_chars))
522 .sum(),
523 serde_json::Value::Object(map) => map
524 .values_mut()
525 .map(|v| truncate_value_recursive(v, max_chars))
526 .sum(),
527 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => 0,
528 }
529}
530
531fn truncate_string_in_place(value: &mut String, max_chars: usize) -> bool {
532 if value.len() <= max_chars {
536 return false;
537 }
538
539 let mut char_count = 0;
540 let mut boundary = value.len();
541 for (idx, _) in value.char_indices() {
542 if char_count == max_chars {
543 boundary = idx;
544 break;
545 }
546 char_count += 1;
547 }
548
549 if char_count < max_chars {
552 return false;
553 }
554
555 let original_char_count = char_count + value[boundary..].chars().count();
556 let truncated = format!(
557 "{}...[truncated, {} chars]",
558 &value[..boundary],
559 original_char_count
560 );
561 *value = truncated;
562 true
563}
564
565fn flatten_and_collect(
567 rows: &[&serde_json::Value],
568) -> (Vec<Map<String, serde_json::Value>>, Vec<String>) {
569 let flat_rows: Vec<Map<String, serde_json::Value>> =
570 rows.iter().map(|r| flatten_row(r)).collect();
571
572 let mut header_set = HashSet::new();
573 let mut headers: Vec<String> = Vec::new();
574 for row in &flat_rows {
575 for key in row.keys() {
576 if header_set.insert(key.clone()) {
577 headers.push(key.clone());
578 }
579 }
580 }
581
582 (flat_rows, headers)
583}
584
585fn flatten_row(value: &serde_json::Value) -> Map<String, serde_json::Value> {
593 let mut out = Map::new();
594 flatten_into(value, "", &mut out);
595 out
596}
597
598fn flatten_into(value: &serde_json::Value, prefix: &str, out: &mut Map<String, serde_json::Value>) {
599 match value {
600 serde_json::Value::Object(map) => {
601 for (k, v) in map {
602 let key = if prefix.is_empty() {
603 k.clone()
604 } else {
605 format!("{prefix}.{k}")
606 };
607 if key.matches('.').count() >= 2 {
609 out.insert(key, v.clone());
610 } else {
611 flatten_into(v, &key, out);
612 }
613 }
614 }
615 _ => {
616 if prefix.is_empty() {
617 out.insert("value".to_string(), value.clone());
618 } else {
619 out.insert(prefix.to_string(), value.clone());
620 }
621 }
622 }
623}
624
625fn format_cell(value: Option<&serde_json::Value>) -> String {
629 match value {
630 None | Some(serde_json::Value::Null) => String::new(),
631 Some(serde_json::Value::String(s)) => {
632 if s.len() > MAX_CELL_LENGTH {
633 let mut end = CELL_TRUNCATE_AT;
634 while end > 0 && !s.is_char_boundary(end) {
635 end -= 1;
636 }
637 format!("{}...", &s[..end])
638 } else {
639 s.clone()
640 }
641 }
642 Some(serde_json::Value::Number(n)) => n.to_string(),
643 Some(serde_json::Value::Bool(b)) => b.to_string(),
644 Some(serde_json::Value::Array(arr)) => {
645 if arr.is_empty() {
646 "[]".to_string()
647 } else if arr.len() <= 3 {
648 let items: Vec<String> = arr.iter().map(|v| format_cell(Some(v))).collect();
650 format!("[{}]", items.join(", "))
651 } else {
652 format!("[{} items]", arr.len())
653 }
654 }
655 Some(serde_json::Value::Object(map)) => {
656 if map.is_empty() {
657 "{}".to_string()
658 } else {
659 format!("{{{} fields}}", map.len())
660 }
661 }
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 fn test_extract_rows_array() {
671 let v = serde_json::json!([{"a": 1}, {"a": 2}]);
672 assert_eq!(extract_rows(&v).len(), 2);
673 }
674
675 #[test]
676 fn test_extract_rows_response_envelope() {
677 let v = serde_json::json!({
678 "status": "success",
679 "data": {
680 "result_type": "matrix",
681 "series": [{"name": "cpu"}, {"name": "mem"}]
682 }
683 });
684 assert_eq!(extract_rows(&v).len(), 2);
685 }
686
687 #[test]
688 fn test_extract_rows_items() {
689 let v = serde_json::json!({
690 "status": "success",
691 "data": {
692 "result_type": "metric_list",
693 "items": ["cpu_usage", "mem_usage"]
694 }
695 });
696 let rows = extract_rows(&v);
697 assert_eq!(rows.len(), 2);
698 }
699
700 #[test]
701 fn test_extract_rows_extension_data_string_array() {
702 let v = serde_json::json!({
703 "status": "success",
704 "data": {
705 "result_type": "services",
706 "data": ["cart", "payment", "frontend"]
707 }
708 });
709 let rows = extract_rows(&v);
710 assert_eq!(rows.len(), 3);
711 }
712
713 #[test]
714 fn test_extract_rows_extension_data_object_array() {
715 let v = serde_json::json!({
716 "status": "success",
717 "data": {
718 "result_type": "services",
719 "data": [
720 {"name": "cart", "spans": 100},
721 {"name": "payment", "spans": 50}
722 ]
723 }
724 });
725 let rows = extract_rows(&v);
726 assert_eq!(rows.len(), 2);
727 }
728
729 #[test]
730 fn test_extract_rows_extension_data_single_object() {
731 let v = serde_json::json!({
732 "status": "success",
733 "data": {
734 "result_type": "config",
735 "data": {"key": "value", "count": 42}
736 }
737 });
738 let rows = extract_rows(&v);
739 assert_eq!(rows.len(), 1);
740 }
741
742 #[test]
743 fn test_flatten_row() {
744 let v = serde_json::json!({"a": {"b": 1}, "c": 2});
745 let flat = flatten_row(&v);
746 assert_eq!(flat["a.b"], serde_json::json!(1));
747 assert_eq!(flat["c"], serde_json::json!(2));
748 }
749
750 #[test]
751 fn test_flatten_row_preserves_field_order() {
752 use crate::model::metric::{DataPoint, MetricSeries, SeriesStats};
755 use std::collections::BTreeMap;
756
757 let series = MetricSeries {
758 name: "cpu".to_string(),
759 labels: BTreeMap::from([("env".to_string(), "prod".to_string())]),
760 points: vec![DataPoint {
761 timestamp: 1,
762 value: 1.0,
763 }],
764 stats: Some(SeriesStats {
765 min: Some(1.0),
766 max: Some(2.0),
767 avg: Some(1.5),
768 count: 2,
769 }),
770 extensions: None,
771 };
772 let value = serde_json::to_value(&series).unwrap();
773 let flat = flatten_row(&value);
774 let keys: Vec<&String> = flat.keys().collect();
775 assert_eq!(keys[0], "name");
777 assert_eq!(keys[1], "labels.env");
778 assert_eq!(keys[2], "points");
779 assert_eq!(keys[3], "stats.min");
781 assert_eq!(keys[4], "stats.max");
782 assert_eq!(keys[5], "stats.avg");
783 assert_eq!(keys[6], "stats.count");
784 }
785
786 #[test]
787 fn test_format_cell_truncation() {
788 let long_str = "a".repeat(100);
789 let result = format_cell(Some(&serde_json::Value::String(long_str)));
790 assert!(result.ends_with("..."));
791 assert!(result.len() <= 63); }
793
794 #[test]
795 fn test_format_cell_array() {
796 let v = serde_json::json!([1, 2, 3]);
797 assert_eq!(format_cell(Some(&v)), "[1, 2, 3]");
798
799 let big = serde_json::json!([1, 2, 3, 4, 5]);
800 assert_eq!(format_cell(Some(&big)), "[5 items]");
801 }
802
803 #[test]
804 fn test_prioritize_headers() {
805 let all = vec![
806 "z_field".to_string(),
807 "name".to_string(),
808 "message".to_string(),
809 "x_field".to_string(),
810 ];
811 let priority = ["name", "message", "severity"];
812 let result = prioritize_headers(&all, &priority, 3);
813 assert_eq!(result, vec!["name", "message", "z_field"]);
814 }
815
816 #[test]
822 fn test_json_output_preserves_struct_field_order() {
823 use crate::model::metric::{DataPoint, MetricSeries, SeriesStats};
824 use std::collections::BTreeMap;
825
826 let series = MetricSeries {
827 name: "cpu".to_string(),
828 labels: BTreeMap::from([("host".to_string(), "web01".to_string())]),
829 points: vec![DataPoint {
830 timestamp: 1,
831 value: 42.0,
832 }],
833 stats: Some(SeriesStats {
834 min: Some(10.0),
835 max: Some(90.0),
836 avg: Some(50.0),
837 count: 5,
838 }),
839 extensions: None,
840 };
841
842 let mut buf = Vec::new();
843 format_and_print(&series, OutputFormat::Json, None, None, &mut buf).unwrap();
844 let output = String::from_utf8(buf).unwrap();
845
846 let min_pos = output.find("\"min\"").unwrap();
848 let max_pos = output.find("\"max\"").unwrap();
849 let avg_pos = output.find("\"avg\"").unwrap();
850 let count_pos = output.find("\"count\"").unwrap();
851 assert!(
852 min_pos < max_pos && max_pos < avg_pos && avg_pos < count_pos,
853 "stats fields should be in struct order: min, max, avg, count\nActual output:\n{output}"
854 );
855 }
856
857 #[test]
858 fn test_project_fields_basic() {
859 let mut value = serde_json::json!({
860 "status": "success",
861 "metadata": {"provider": "vm"},
862 "data": {
863 "result_type": "spans",
864 "spans": [
865 {"service": "api", "name": "GET /users", "status": "ok"}
866 ]
867 }
868 });
869
870 project_fields(&mut value, &["service".to_string(), "name".to_string()]);
871
872 assert_eq!(
873 value["data"]["spans"][0],
874 serde_json::json!({"service": "api", "name": "GET /users"})
875 );
876 }
877
878 #[test]
879 fn test_project_fields_dot_notation() {
880 let mut value = serde_json::json!({
881 "status": "success",
882 "data": {
883 "result_type": "matrix",
884 "series": [
885 {"name": "cpu", "labels": {"env": "prod", "host": "web01"}}
886 ]
887 }
888 });
889
890 project_fields(&mut value, &["labels.env".to_string()]);
891
892 assert_eq!(
893 value["data"]["series"][0],
894 serde_json::json!({"labels": {"env": "prod"}})
895 );
896 }
897
898 #[test]
899 fn test_project_fields_broader_wins() {
900 let mut value = serde_json::json!({
901 "status": "success",
902 "data": {
903 "result_type": "matrix",
904 "series": [
905 {"labels": {"env": "prod", "host": "web01"}}
906 ]
907 }
908 });
909
910 project_fields(
911 &mut value,
912 &["labels.env".to_string(), "labels".to_string()],
913 );
914
915 assert_eq!(
916 value["data"]["series"][0],
917 serde_json::json!({"labels": {"env": "prod", "host": "web01"}})
918 );
919 }
920
921 #[test]
922 fn test_project_fields_absent_field() {
923 let mut value = serde_json::json!({
924 "status": "success",
925 "data": {
926 "result_type": "log_entries",
927 "entries": [
928 {"timestamp": 1, "message": "hello"}
929 ]
930 }
931 });
932
933 project_fields(&mut value, &["service".to_string()]);
934
935 assert_eq!(value["data"]["entries"][0], serde_json::json!({}));
936 }
937
938 #[test]
939 fn test_project_fields_with_response_envelope() {
940 let mut value = serde_json::json!({
941 "status": "success",
942 "metadata": {"provider": "vm", "total_count": 1},
943 "data": {
944 "result_type": "trace_detail",
945 "trace_id": "abc123",
946 "span_count": 2,
947 "service_count": 1,
948 "duration_us": 100,
949 "services": ["api"],
950 "spans": [
951 {"service": "api", "name": "root", "attributes": {"env": "prod"}}
952 ]
953 }
954 });
955
956 project_fields(&mut value, &["service".to_string()]);
957
958 assert_eq!(value["status"], "success");
959 assert_eq!(value["metadata"]["provider"], "vm");
960 assert_eq!(value["data"]["trace_id"], "abc123");
961 assert_eq!(value["data"]["span_count"], 2);
962 assert_eq!(value["data"]["services"], serde_json::json!(["api"]));
964 assert_eq!(
965 value["data"]["spans"][0],
966 serde_json::json!({"service": "api"})
967 );
968 }
969
970 #[test]
971 fn test_truncate_basic() {
972 let mut value = serde_json::json!({
973 "status": "success",
974 "metadata": {"provider": "vm"},
975 "data": {
976 "result_type": "log_entries",
977 "entries": [{"message": "abcdefghij"}]
978 }
979 });
980
981 truncate_values(&mut value, 5);
982
983 assert_eq!(
984 value["data"]["entries"][0]["message"],
985 "abcde...[truncated, 10 chars]"
986 );
987 }
988
989 #[test]
990 fn test_truncate_under_limit() {
991 let mut value = serde_json::json!({
992 "status": "success",
993 "data": {
994 "result_type": "log_entries",
995 "entries": [{"message": "short"}]
996 }
997 });
998
999 truncate_values(&mut value, 10);
1000
1001 assert_eq!(value["data"]["entries"][0]["message"], "short");
1002 }
1003
1004 #[test]
1005 fn test_truncate_recursive() {
1006 let mut value = serde_json::json!({
1007 "status": "success",
1008 "data": {
1009 "result_type": "spans",
1010 "spans": [{
1011 "attributes": {
1012 "http.body": "abcdefghij",
1013 "nested": ["klmnopqrstu"]
1014 }
1015 }]
1016 }
1017 });
1018
1019 truncate_values(&mut value, 4);
1020
1021 assert_eq!(
1022 value["data"]["spans"][0]["attributes"]["http.body"],
1023 "abcd...[truncated, 10 chars]"
1024 );
1025 assert_eq!(
1026 value["data"]["spans"][0]["attributes"]["nested"][0],
1027 "klmn...[truncated, 11 chars]"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_truncate_non_string() {
1033 let mut value = serde_json::json!({
1034 "status": "success",
1035 "data": {
1036 "result_type": "spans",
1037 "spans": [{"duration_us": 123, "ok": true, "missing": null}]
1038 }
1039 });
1040
1041 truncate_values(&mut value, 2);
1042
1043 assert_eq!(value["data"]["spans"][0]["duration_us"], 123);
1044 assert_eq!(value["data"]["spans"][0]["ok"], true);
1045 assert_eq!(
1046 value["data"]["spans"][0]["missing"],
1047 serde_json::Value::Null
1048 );
1049 }
1050
1051 #[test]
1052 fn test_truncate_char_boundary() {
1053 let mut value = serde_json::json!({
1054 "status": "success",
1055 "data": {
1056 "result_type": "log_entries",
1057 "entries": [{"message": "你好世界"}]
1058 }
1059 });
1060
1061 truncate_values(&mut value, 3);
1062
1063 assert_eq!(
1064 value["data"]["entries"][0]["message"],
1065 "你好世...[truncated, 4 chars]"
1066 );
1067 }
1068
1069 #[test]
1070 fn test_truncate_marker_format() {
1071 let mut value = serde_json::json!({
1072 "status": "success",
1073 "data": {
1074 "result_type": "log_entries",
1075 "entries": [{"message": "123456789"}]
1076 }
1077 });
1078
1079 truncate_values(&mut value, 2);
1080
1081 assert_eq!(
1082 value["data"]["entries"][0]["message"],
1083 "12...[truncated, 9 chars]"
1084 );
1085 }
1086
1087 #[test]
1088 fn test_combined_fields_then_truncate() {
1089 let value = serde_json::json!({
1090 "status": "success",
1091 "metadata": {"provider": "dd"},
1092 "data": {
1093 "result_type": "log_entries",
1094 "entries": [{
1095 "timestamp": 1,
1096 "service": "api",
1097 "message": "abcdefghij",
1098 "attributes": {"env": "prod"}
1099 }]
1100 }
1101 });
1102
1103 let mut buf = Vec::new();
1104 let fields = ["service".to_string(), "message".to_string()];
1105 format_and_print(&value, OutputFormat::Json, Some(&fields), Some(5), &mut buf).unwrap();
1106 let output: serde_json::Value = serde_json::from_slice(&buf).unwrap();
1107
1108 assert_eq!(
1109 output["data"]["entries"][0],
1110 serde_json::json!({
1111 "service": "api",
1112 "message": "abcde...[truncated, 10 chars]"
1113 })
1114 );
1115 assert_eq!(output["metadata"]["truncated_values"], 1);
1116 }
1117
1118 #[test]
1119 fn test_truncate_no_metadata_when_nothing_truncated() {
1120 let value = serde_json::json!({
1121 "status": "success",
1122 "metadata": {"provider": "vm"},
1123 "data": {
1124 "result_type": "log_entries",
1125 "entries": [{"message": "short"}]
1126 }
1127 });
1128
1129 let mut buf = Vec::new();
1130 format_and_print(&value, OutputFormat::Json, None, Some(100), &mut buf).unwrap();
1131 let output: serde_json::Value = serde_json::from_slice(&buf).unwrap();
1132
1133 assert!(output["metadata"]["truncated_values"].is_null());
1134 }
1135
1136 #[test]
1138 fn test_csv_output_preserves_field_order() {
1139 use crate::model::metric::{DataPoint, MetricSeries, SeriesStats};
1140 use std::collections::BTreeMap;
1141
1142 let series = vec![MetricSeries {
1143 name: "cpu".to_string(),
1144 labels: BTreeMap::from([("env".to_string(), "prod".to_string())]),
1145 points: vec![DataPoint {
1146 timestamp: 1,
1147 value: 42.0,
1148 }],
1149 stats: Some(SeriesStats {
1150 min: Some(10.0),
1151 max: Some(90.0),
1152 avg: Some(50.0),
1153 count: 5,
1154 }),
1155 extensions: None,
1156 }];
1157
1158 let mut buf = Vec::new();
1159 format_and_print(&series, OutputFormat::Csv, None, None, &mut buf).unwrap();
1160 let output = String::from_utf8(buf).unwrap();
1161
1162 let header_line = output.lines().next().unwrap();
1163 let headers: Vec<&str> = header_line.split(',').collect();
1164 assert_eq!(headers[0], "name");
1166 let min_idx = headers.iter().position(|h| *h == "stats.min").unwrap();
1168 let count_idx = headers.iter().position(|h| *h == "stats.count").unwrap();
1169 assert!(
1170 min_idx < count_idx,
1171 "CSV headers should follow struct field order, got: {header_line}"
1172 );
1173 }
1174
1175 #[test]
1177 fn test_flatten_row_dynamic_json_preserves_insertion_order() {
1178 let v = serde_json::json!({
1182 "status": "success",
1183 "data": {
1184 "result_type": "custom",
1185 "data": {
1186 "zebra": 1,
1187 "alpha": 2,
1188 "middle": 3
1189 }
1190 }
1191 });
1192
1193 let rows = extract_rows(&v);
1195 assert_eq!(rows.len(), 1);
1196 let flat = flatten_row(rows[0]);
1197 let keys: Vec<&String> = flat.keys().collect();
1198 assert_eq!(keys, &["zebra", "alpha", "middle"]);
1201 }
1202
1203 #[test]
1204 fn test_truncate_preserves_result_type() {
1205 let value = serde_json::json!({
1206 "status": "success",
1207 "metadata": {"provider": "vm"},
1208 "data": {
1209 "result_type": "log_entries",
1210 "entries": [{"message": "abcdefghij"}]
1211 }
1212 });
1213
1214 let mut buf = Vec::new();
1215 format_and_print(&value, OutputFormat::Json, None, Some(3), &mut buf).unwrap();
1216 let output: serde_json::Value = serde_json::from_slice(&buf).unwrap();
1217
1218 assert_eq!(output["data"]["result_type"], "log_entries");
1219 assert_eq!(
1220 output["data"]["entries"][0]["message"],
1221 "abc...[truncated, 10 chars]"
1222 );
1223 }
1224
1225 #[test]
1226 fn test_truncate_preserves_trace_summary() {
1227 let value = serde_json::json!({
1228 "status": "success",
1229 "metadata": {"provider": "vm"},
1230 "data": {
1231 "result_type": "trace_detail",
1232 "trace_id": "abc123def456",
1233 "span_count": 5,
1234 "services": ["api-gateway", "payment"],
1235 "spans": [
1236 {"service": "api-gateway", "name": "long-operation-name-that-exceeds"}
1237 ]
1238 }
1239 });
1240
1241 let mut buf = Vec::new();
1242 format_and_print(&value, OutputFormat::Json, None, Some(10), &mut buf).unwrap();
1243 let output: serde_json::Value = serde_json::from_slice(&buf).unwrap();
1244
1245 assert_eq!(output["data"]["trace_id"], "abc123def456");
1246 assert_eq!(output["data"]["result_type"], "trace_detail");
1247 assert_eq!(
1248 output["data"]["services"],
1249 serde_json::json!(["api-gateway", "payment"])
1250 );
1251 assert!(output["data"]["spans"][0]["name"]
1252 .as_str()
1253 .unwrap()
1254 .contains("[truncated"));
1255 }
1256
1257 #[test]
1258 fn test_extract_rows_spans() {
1259 let v = serde_json::json!({
1260 "status": "success",
1261 "data": {
1262 "result_type": "spans",
1263 "spans": [
1264 {"service": "api", "name": "GET"},
1265 {"service": "db", "name": "SELECT"}
1266 ]
1267 }
1268 });
1269 assert_eq!(extract_rows(&v).len(), 2);
1270 }
1271
1272 #[test]
1273 fn test_inject_truncated_count_no_metadata() {
1274 let mut value = serde_json::json!({"data": {"entries": []}});
1275 inject_truncated_count(&mut value, 5);
1276 assert!(value.get("metadata").is_none());
1277 }
1278}