spreadsheet_kit/cli/
output.rs1use crate::cli::{OutputFormat, OutputShape};
2use crate::response_prune::prune_non_structural_empties;
3use anyhow::{Result, bail};
4use serde_json::{Map, Value};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum CompactProjectionTarget {
8 None,
9 RangeValues,
10 ReadTable,
11 SheetPage,
12 FormulaTrace,
13}
14
15pub fn emit_value(
16 value: &Value,
17 format: OutputFormat,
18 shape: OutputShape,
19 projection_target: CompactProjectionTarget,
20 compact: bool,
21 quiet: bool,
22) -> Result<()> {
23 if matches!(format, OutputFormat::Csv) {
24 bail!("csv output is not implemented yet for agent-spreadsheet")
25 }
26
27 let mut value = value.clone();
28 prune_non_structural_empties(&mut value);
29 apply_shape(&mut value, shape, projection_target);
30
31 let stdout = std::io::stdout();
32 let mut handle = stdout.lock();
33 let _ = (compact, quiet);
34 serde_json::to_writer(&mut handle, &value)?;
35 use std::io::Write;
36 handle.write_all(b"\n")?;
37 Ok(())
38}
39
40fn apply_shape(value: &mut Value, shape: OutputShape, projection_target: CompactProjectionTarget) {
41 if !matches!(shape, OutputShape::Compact) {
42 return;
43 }
44
45 match projection_target {
46 CompactProjectionTarget::None => {}
47 CompactProjectionTarget::RangeValues => project_range_values_compact(value),
48 CompactProjectionTarget::ReadTable => project_read_table_compact(value),
49 CompactProjectionTarget::SheetPage => project_sheet_page_compact(value),
50 CompactProjectionTarget::FormulaTrace => project_formula_trace_compact(value),
51 }
52}
53
54fn project_range_values_compact(value: &mut Value) {
55 let _ = value;
58}
59
60fn project_read_table_compact(value: &mut Value) {
61 let Value::Object(obj) = value else {
64 return;
65 };
66 drop_empty_wrappers(obj, &["headers", "types"]);
67}
68
69fn project_sheet_page_compact(value: &mut Value) {
70 let _ = value;
74}
75
76fn project_formula_trace_compact(value: &mut Value) {
77 let Some(layers) = value.get_mut("layers").and_then(Value::as_array_mut) else {
80 return;
81 };
82
83 for layer in layers {
84 if let Value::Object(layer_obj) = layer {
85 layer_obj.remove("highlights");
86 }
87 }
88}
89
90fn drop_empty_wrappers(target: &mut Map<String, Value>, keys: &[&str]) {
91 for key in keys {
92 let should_remove = target
93 .get(*key)
94 .map(|value| {
95 value.is_null()
96 || value.as_array().is_some_and(Vec::is_empty)
97 || value.as_object().is_some_and(Map::is_empty)
98 })
99 .unwrap_or(false);
100 if should_remove {
101 target.remove(*key);
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use serde_json::json;
110
111 #[test]
112 fn compact_shape_3109_formula_trace_omits_highlights_only_for_trace_target() {
113 let mut compact_payload = json!({
114 "workbook_id": "wb",
115 "sheet_name": "Sheet1",
116 "origin": "A1",
117 "direction": "dependents",
118 "next_cursor": { "depth": 1, "offset": 5 },
119 "notes": ["note"],
120 "layers": [{
121 "depth": 1,
122 "summary": { "total_nodes": 1 },
123 "highlights": { "top_ranges": [], "top_formula_groups": [], "notable_cells": [] },
124 "edges": [{ "from": "A1", "to": "B1" }],
125 "has_more": true
126 }]
127 });
128
129 apply_shape(
130 &mut compact_payload,
131 OutputShape::Compact,
132 CompactProjectionTarget::FormulaTrace,
133 );
134
135 let layer = compact_payload["layers"]
136 .as_array()
137 .expect("layers array")
138 .first()
139 .cloned()
140 .expect("first layer");
141 assert!(layer.get("highlights").is_none());
142 assert_eq!(layer["depth"], json!(1));
143 assert!(layer.get("summary").is_some());
144 assert!(layer.get("edges").is_some());
145 assert_eq!(layer["has_more"], json!(true));
146 assert_eq!(compact_payload["next_cursor"]["depth"], json!(1));
147 assert_eq!(compact_payload["next_cursor"]["offset"], json!(5));
148 }
149
150 #[test]
151 fn compact_shape_3109_none_target_does_not_apply_trace_projection() {
152 let mut payload = json!({
153 "layers": [{
154 "depth": 1,
155 "summary": { "total_nodes": 1 },
156 "highlights": { "top_ranges": [] },
157 "edges": [],
158 "has_more": false
159 }]
160 });
161
162 apply_shape(
163 &mut payload,
164 OutputShape::Compact,
165 CompactProjectionTarget::None,
166 );
167
168 let layer = payload["layers"]
169 .as_array()
170 .expect("layers array")
171 .first()
172 .expect("first layer");
173 assert!(layer.get("highlights").is_some());
174 }
175
176 #[test]
177 fn compact_shape_3109_range_values_keeps_stable_shape() {
178 let base_payload = json!({
179 "workbook_id": "wb",
180 "sheet_name": "Sheet1",
181 "values": [{
182 "range": "A1:B2",
183 "rows": [[{"kind":"text","value":"x"}]],
184 "next_start_row": 3
185 }]
186 });
187
188 let mut compact_range_values = base_payload.clone();
189 apply_shape(
190 &mut compact_range_values,
191 OutputShape::Compact,
192 CompactProjectionTarget::RangeValues,
193 );
194 assert!(compact_range_values.get("values").is_some());
195 assert!(compact_range_values.get("range").is_none());
196
197 let mut compact_none_target = base_payload;
198 apply_shape(
199 &mut compact_none_target,
200 OutputShape::Compact,
201 CompactProjectionTarget::None,
202 );
203 assert!(compact_none_target.get("values").is_some());
204 assert!(compact_none_target.get("range").is_none());
205 }
206}