omni_dev/drive/sheets/types.rs
1//! Wire types for the Sheets v4 REST API.
2//!
3//! Field naming follows Sheets' camelCase JSON via per-field
4//! `#[serde(rename = "...")]`, mirroring `src/drive/types.rs` and
5//! `src/gmail/types.rs`. Only the subset the CLI actually renders is
6//! modelled; every unmodelled field is tolerated and dropped, so a Google
7//! response gaining a field never breaks a parse.
8//!
9//! Two shapes here are load-bearing and easy to get wrong:
10//!
11//! - **`ValueRange::values` is absent, not empty, for an empty sheet.** It
12//! needs `#[serde(default)]` or a blank tab fails the whole read.
13//! - **Rows are ragged.** The API truncates trailing empty cells from each
14//! row, and trailing empty rows entirely, so row 1 may have 5 cells and
15//! row 2 only 2. Renderers must decide explicitly what to do about that
16//! rather than assuming a rectangle.
17//! - **Sheets speaks proto3 JSON, where a scalar's zero value (`0`, `0.0`,
18//! `false`, `""`) is omitted from the wire entirely**, not sent
19//! explicitly. A required, non-`Option` scalar field (`i32`/`i64`/`f32`/
20//! `f64`/`bool`/`String`) on any `Deserialize` struct here therefore
21//! needs a field-level `#[serde(default)]`, or a value of exactly zero —
22//! the first sheet, row 1, column A, an unset color channel, a `false`
23//! flag, an empty pattern — makes the whole parse fail, bricking every
24//! read of a spreadsheet in that state (issue #1929). This has nothing to
25//! do with whether the field is ever *sent* that way: `#[serde(default)]`
26//! only affects deserialisation, so a field this crate always sends
27//! explicitly (like [`PivotGroup::show_totals`]) keeps doing so. Give the
28//! next scalar field added here the same treatment on sight, rather than
29//! waiting for a live failure to reveal it.
30
31use std::collections::BTreeMap;
32
33use serde::{Deserialize, Serialize};
34
35/// A spreadsheet's metadata, from `spreadsheets.get`.
36///
37/// Always request an explicit `fields` mask when fetching this: the
38/// unmasked response embeds **every cell of every sheet**, which on a large
39/// workbook is an out-of-memory failure rather than a slow request.
40///
41/// `PartialEq`-only, not `Eq` (issue #1793): a [`Sheet`]'s
42/// `conditional_formats` can embed a `Color` (`f32`-based), which has no
43/// meaningful `Eq` — see [`Color`]'s own doc comment.
44#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
45pub struct Spreadsheet {
46 /// The spreadsheet's id (echoes the one requested).
47 #[serde(
48 default,
49 skip_serializing_if = "Option::is_none",
50 rename = "spreadsheetId"
51 )]
52 pub spreadsheet_id: Option<String>,
53 /// Workbook-level properties, notably its title.
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub properties: Option<SpreadsheetProperties>,
56 /// The sheets (tabs) it contains, in workbook order.
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub sheets: Vec<Sheet>,
59 /// Named ranges defined anywhere in the workbook. A named range is
60 /// workbook-scoped, not sheet-scoped (unlike [`ProtectedRange`], which
61 /// nests under the [`Sheet`] it protects), so this lives here rather
62 /// than on `Sheet`. Empty unless the caller requested it with a wider
63 /// `fields` mask — `SPREADSHEET_FIELDS` (the mask every other call uses)
64 /// omits this; only `SheetsApi::get_spreadsheet_with_named_ranges`
65 /// populates it.
66 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "namedRanges")]
67 pub named_ranges: Vec<NamedRange>,
68}
69
70impl Spreadsheet {
71 /// The workbook title, or `""` when the `fields` mask omitted it.
72 #[must_use]
73 pub fn title(&self) -> &str {
74 self.properties
75 .as_ref()
76 .map_or("", |props| props.title.as_str())
77 }
78
79 /// Every sheet title, in workbook order.
80 #[must_use]
81 pub fn sheet_titles(&self) -> Vec<String> {
82 self.sheets
83 .iter()
84 .map(|sheet| sheet.title().to_string())
85 .collect()
86 }
87
88 /// Every sheet id, for sheets that carry one.
89 #[must_use]
90 pub fn sheet_ids(&self) -> std::collections::HashSet<i64> {
91 self.sheets.iter().filter_map(Sheet::sheet_id).collect()
92 }
93
94 /// Whether any sheet in the workbook currently uses `title` as its
95 /// display title — the collision check `drive sheets rename-sheet`/
96 /// `duplicate-sheet` (via `structure.rs`'s `resolve_sheet`) and `drive
97 /// lease restore`'s best-effort rename-back (issue #1676) share, so a
98 /// future change to what counts as "the same title" (e.g.
99 /// case-insensitive) lands in one place rather than drifting between
100 /// two hand-written copies.
101 ///
102 /// A sheet with no `properties` never matches, not even `""` — it has no
103 /// title at all, so `Sheet::title()`'s `""` fallback (meant for display)
104 /// would otherwise falsely collide with an explicit empty-title request.
105 #[must_use]
106 pub fn has_sheet_titled(&self, title: &str) -> bool {
107 self.sheets
108 .iter()
109 .filter_map(|sheet| sheet.properties.as_ref())
110 .any(|props| props.title == title)
111 }
112}
113
114/// Workbook-level properties.
115///
116/// `PartialEq`-only, not `Eq` (issue #1836): [`IterativeCalculationSettings`]
117/// carries an `f64`, which has no meaningful `Eq` — see [`Spreadsheet`]'s own
118/// doc comment for the same reasoning applied to [`Color`].
119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
120pub struct SpreadsheetProperties {
121 /// The workbook's display title.
122 #[serde(default)]
123 pub title: String,
124 /// The workbook's locale, e.g. `"en_US"` (issue #1836's
125 /// `update-workbook-properties --locale`).
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub locale: Option<String>,
128 /// The workbook's IANA time zone, e.g. `"America/New_York"`
129 /// (`update-workbook-properties --time-zone`).
130 #[serde(default, skip_serializing_if = "Option::is_none", rename = "timeZone")]
131 pub time_zone: Option<String>,
132 /// How often the workbook recalculates
133 /// (`update-workbook-properties --auto-recalc`).
134 #[serde(
135 default,
136 skip_serializing_if = "Option::is_none",
137 rename = "autoRecalc"
138 )]
139 pub auto_recalc: Option<RecalculationInterval>,
140 /// Iterative-calculation settings, present only when iterative
141 /// calculation is on (`update-workbook-properties --iterative-calculation`).
142 /// See [`IterativeCalculationSettings`]'s doc comment for why presence,
143 /// not a boolean field, is what "on" means.
144 #[serde(
145 default,
146 skip_serializing_if = "Option::is_none",
147 rename = "iterativeCalculationSettings"
148 )]
149 pub iterative_calculation_settings: Option<IterativeCalculationSettings>,
150}
151
152/// A workbook's automatic-recalculation interval (`autoRecalc`).
153#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
154pub enum RecalculationInterval {
155 /// Sheets' way of saying the field was never set. Never something
156 /// `update-workbook-properties --auto-recalc` writes — see
157 /// [`crate::cli::drive::sheets::structure::AutoRecalcArg`]'s doc comment
158 /// for why the CLI-facing enum omits it.
159 #[serde(rename = "RECALCULATION_INTERVAL_UNSPECIFIED")]
160 Unspecified,
161 /// Recalculate on every edit (Sheets' own default).
162 #[serde(rename = "ON_CHANGE")]
163 OnChange,
164 /// Recalculate at most once a minute.
165 #[serde(rename = "MINUTE")]
166 Minute,
167 /// Recalculate at most once an hour.
168 #[serde(rename = "HOUR")]
169 Hour,
170}
171
172impl RecalculationInterval {
173 /// The wire spelling, also reused in human-readable output (the
174 /// `update-workbook-properties` dry-run preview, the real-run
175 /// confirmation, and the request log's `fields_changed`).
176 #[must_use]
177 pub fn as_str(self) -> &'static str {
178 match self {
179 Self::Unspecified => "RECALCULATION_INTERVAL_UNSPECIFIED",
180 Self::OnChange => "ON_CHANGE",
181 Self::Minute => "MINUTE",
182 Self::Hour => "HOUR",
183 }
184 }
185}
186
187/// A workbook's iterative-calculation settings (`iterativeCalculationSettings`).
188///
189/// Sheets has no boolean "enabled" field: this object's mere *presence* on
190/// [`SpreadsheetProperties`] is what turns iterative calculation on, and its
191/// *absence* is what turns it off. `update-workbook-properties
192/// --iterative-calculation off` therefore clears the field entirely (an
193/// absent key in the field-masked request, via
194/// [`SpreadsheetPropertiesUpdate::iterative_calculation_settings`]) rather
195/// than writing some "disabled" value into it — there is no such value.
196///
197/// Turning iterative calculation on changes what a circular-reference
198/// formula elsewhere in the workbook *evaluates to*: a value effect reached
199/// indirectly, the same shape of concern ADR-0081 raised for named-range
200/// deletion. It is still gated as `sheets-structure` rather than a data-
201/// mutating operation, because — like a named-range deletion — no cell's
202/// formula is itself changed, only what some formulas compute (issue #1836,
203/// [ADR-0086](../../../docs/adrs/adr-0086-workbook-properties.md) §9).
204///
205/// `PartialEq`-only, not `Eq`: `convergence_threshold` is an `f64`.
206#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
207#[serde(rename_all = "camelCase")]
208pub struct IterativeCalculationSettings {
209 /// Maximum number of calculation rounds per recalculation. Omitted in a
210 /// request takes Sheets' own default.
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub max_iterations: Option<i64>,
213 /// The maximum change between two consecutive rounds that still counts
214 /// as converged. Omitted in a request takes Sheets' own default.
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub convergence_threshold: Option<f64>,
217}
218
219impl IterativeCalculationSettings {
220 /// A human-readable qualifier for whichever bounds are set, e.g.
221 /// `" (max 50 iterations, threshold 0.01)"` — empty (not just the
222 /// bounds omitted) when both take Sheets' own default. The leading
223 /// space lets every call site append it directly after `"on"`.
224 ///
225 /// Shared by `update-workbook-properties`'s dry-run preview and
226 /// real-run confirmation (`structure.rs::workbook_properties_summary`)
227 /// and `sheets info`'s table render, so the two can never describe the
228 /// same settings differently.
229 #[must_use]
230 pub fn describe_bounds(&self) -> String {
231 match (self.max_iterations, self.convergence_threshold) {
232 (Some(max), Some(threshold)) => {
233 format!(" (max {max} iterations, threshold {threshold})")
234 }
235 (Some(max), None) => format!(" (max {max} iterations)"),
236 (None, Some(threshold)) => format!(" (threshold {threshold})"),
237 (None, None) => String::new(),
238 }
239 }
240}
241
242/// One sheet (tab) within a spreadsheet.
243/// `PartialEq`-only, not `Eq` (issue #1793) — see [`Spreadsheet`]'s doc
244/// comment.
245#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
246pub struct Sheet {
247 /// This sheet's properties.
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub properties: Option<SheetProperties>,
250 /// Protected ranges on this sheet. Empty unless the caller requested
251 /// them with a wider `fields` mask — `SPREADSHEET_FIELDS` (the mask
252 /// every other call uses) omits this, so a plain `sheets info`/
253 /// `structure` read never pays for it; only
254 /// `SheetsApi::get_spreadsheet_with_protections` populates it.
255 #[serde(
256 default,
257 skip_serializing_if = "Vec::is_empty",
258 rename = "protectedRanges"
259 )]
260 pub protected_ranges: Vec<ProtectedRange>,
261 /// This sheet's basic filter, if it has one. Empty unless the caller
262 /// requested it with a wider `fields` mask — only
263 /// `SheetsApi::get_spreadsheet_with_filter_views` populates it
264 /// (issue #1794).
265 #[serde(
266 default,
267 skip_serializing_if = "Option::is_none",
268 rename = "basicFilter"
269 )]
270 pub basic_filter: Option<BasicFilter>,
271 /// The named, id-addressed filter views on this sheet. Empty unless the
272 /// caller requested them with a wider `fields` mask — only
273 /// `SheetsApi::get_spreadsheet_with_filter_views` populates it
274 /// (issue #1794).
275 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "filterViews")]
276 pub filter_views: Vec<FilterView>,
277 /// Conditional format rules on this sheet, in their API-defined
278 /// evaluation order — the order `add-conditional-format`'s `--index`/
279 /// `update-conditional-format`'s and `delete-conditional-format`'s
280 /// `--index` address. Empty unless the caller requested a wider
281 /// `fields` mask; only `SheetsApi::get_spreadsheet_with_conditional_formats`
282 /// populates it (issue #1793).
283 #[serde(
284 default,
285 skip_serializing_if = "Vec::is_empty",
286 rename = "conditionalFormats"
287 )]
288 pub conditional_formats: Vec<ConditionalFormatRule>,
289 /// Charts embedded on this sheet. Empty unless the caller requested
290 /// them with a wider `fields` mask — only
291 /// `SheetsApi::get_spreadsheet_with_embedded_objects` populates it
292 /// (issue #1797). `chartId`-addressed by `update-chart`/`delete-chart`;
293 /// `list-charts` is how that id is discovered.
294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 pub charts: Vec<EmbeddedChart>,
296 /// Slicers embedded on this sheet. Empty unless the caller requested
297 /// them with a wider `fields` mask — only
298 /// `SheetsApi::get_spreadsheet_with_embedded_objects` populates it
299 /// (issue #1797). `slicerId`-addressed by `update-slicer`/
300 /// `delete-slicer`; `list-slicers` is how that id is discovered.
301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
302 pub slicers: Vec<Slicer>,
303 /// Banded ranges (alternating row/column colors) on this sheet. Empty
304 /// unless the caller requested them with a wider `fields` mask — only
305 /// `SheetsApi::get_spreadsheet_with_banding` populates it (issue
306 /// #1832). `bandedRangeId`-addressed by `update-banding`/
307 /// `delete-banding`; `list-bandings` is how that id is discovered.
308 #[serde(
309 default,
310 skip_serializing_if = "Vec::is_empty",
311 rename = "bandedRanges"
312 )]
313 pub banded_ranges: Vec<BandedRange>,
314 /// Outline groups over row spans on this sheet — the collapsible +/-
315 /// grouping bar. Empty unless the caller requested them with a wider
316 /// `fields` mask — only
317 /// `SheetsApi::get_spreadsheet_with_dimension_groups` populates it
318 /// (issue #1833). Addressed by `(range, depth)`;
319 /// `list-dimension-groups` is how both are discovered.
320 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "rowGroups")]
321 pub row_groups: Vec<DimensionGroup>,
322 /// Outline groups over column spans on this sheet. Same population and
323 /// addressing as [`Self::row_groups`], the column axis.
324 #[serde(
325 default,
326 skip_serializing_if = "Vec::is_empty",
327 rename = "columnGroups"
328 )]
329 pub column_groups: Vec<DimensionGroup>,
330 /// Grid data — cell-by-cell, in row-major chunks — read back only when
331 /// the caller asked for it via a `fields` mask naming individual cell
332 /// properties. Empty unless the caller requested a wider mask; only
333 /// [`SheetsApi::get_spreadsheet_with_pivot_tables`](crate::drive::sheets::api::SheetsApi::get_spreadsheet_with_pivot_tables)/
334 /// `SheetsApi::get_cell_pivot` populate it (issue #1798). Chunked
335 /// rather than one flat grid because the API itself returns it that
336 /// way — see [`GridData`]'s doc comment.
337 #[serde(default, skip_serializing_if = "Vec::is_empty")]
338 pub data: Vec<GridData>,
339}
340
341impl Sheet {
342 /// This sheet's title, or `""` if absent.
343 #[must_use]
344 pub fn title(&self) -> &str {
345 self.properties
346 .as_ref()
347 .map_or("", |props| props.title.as_str())
348 }
349
350 /// This sheet's stable numeric id, or `None` if the properties (or the
351 /// id within them) are absent from the response.
352 #[must_use]
353 pub fn sheet_id(&self) -> Option<i64> {
354 self.properties.as_ref()?.sheet_id
355 }
356
357 /// Whether the sheet is hidden in the UI.
358 ///
359 /// Hidden sheets are deliberately **included** in a full read: omitting
360 /// them would silently drop data the caller asked for, and "fail
361 /// visible" is the safer default for a data-extraction tool.
362 #[must_use]
363 pub fn hidden(&self) -> bool {
364 self.properties
365 .as_ref()
366 .is_some_and(|props| props.hidden.unwrap_or(false))
367 }
368}
369
370/// A sheet's properties.
371#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
372pub struct SheetProperties {
373 /// Stable numeric id, unique within the spreadsheet.
374 #[serde(default, skip_serializing_if = "Option::is_none", rename = "sheetId")]
375 pub sheet_id: Option<i64>,
376 /// Display title — the string used to build an A1 prefix.
377 #[serde(default)]
378 pub title: String,
379 /// Position in the workbook, zero-based.
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub index: Option<i64>,
382 /// Whether the sheet is hidden in the UI.
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub hidden: Option<bool>,
385 /// Grid dimensions, when the sheet is a grid.
386 #[serde(
387 default,
388 skip_serializing_if = "Option::is_none",
389 rename = "gridProperties"
390 )]
391 pub grid_properties: Option<GridProperties>,
392 /// Whether the sheet is laid out right-to-left (issue #1835). Not
393 /// paired with a `tab_color_style` field here — see
394 /// [`SheetPropertiesUpdate::tab_color_style`]'s doc comment for why the
395 /// tab color is write-only and deliberately never read back onto this
396 /// response type.
397 #[serde(
398 default,
399 skip_serializing_if = "Option::is_none",
400 rename = "rightToLeft"
401 )]
402 pub right_to_left: Option<bool>,
403}
404
405/// A grid sheet's declared dimensions.
406///
407/// These are the sheet's *allocated* size, not the extent of its data — a
408/// blank sheet still reports 1000 x 26.
409#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
410pub struct GridProperties {
411 /// Number of rows allocated.
412 #[serde(default, skip_serializing_if = "Option::is_none", rename = "rowCount")]
413 pub row_count: Option<i64>,
414 /// Number of columns allocated.
415 #[serde(
416 default,
417 skip_serializing_if = "Option::is_none",
418 rename = "columnCount"
419 )]
420 pub column_count: Option<i64>,
421 /// Rows frozen at the top (issue #1835).
422 #[serde(
423 default,
424 skip_serializing_if = "Option::is_none",
425 rename = "frozenRowCount"
426 )]
427 pub frozen_row_count: Option<i64>,
428 /// Columns frozen at the left (issue #1835).
429 #[serde(
430 default,
431 skip_serializing_if = "Option::is_none",
432 rename = "frozenColumnCount"
433 )]
434 pub frozen_column_count: Option<i64>,
435 /// Whether gridlines are hidden in the UI (issue #1835).
436 #[serde(
437 default,
438 skip_serializing_if = "Option::is_none",
439 rename = "hideGridlines"
440 )]
441 pub hide_gridlines: Option<bool>,
442}
443
444/// Cell values for one range, from `values.get` / `values.batchGet`.
445#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
446pub struct ValueRange {
447 /// The range these values came from, **server-normalised** (e.g.
448 /// `'My Sheet'!A1:C7`).
449 ///
450 /// This is what a batch response must be matched on. Zipping a
451 /// `batchGet` reply to the request list by index instead would silently
452 /// misattribute one sheet's data to another the moment the server
453 /// reorders, merges or drops a range.
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub range: Option<String>,
456 /// Row-major cell values. **Absent — not empty — for a sheet with no
457 /// data**, hence `#[serde(default)]`.
458 ///
459 /// Rows are ragged: trailing empty cells are truncated per row, and
460 /// trailing empty rows dropped entirely.
461 #[serde(default, skip_serializing_if = "Vec::is_empty")]
462 pub values: Vec<Vec<serde_json::Value>>,
463}
464
465impl ValueRange {
466 /// The widest row, i.e. the column count needed to render this range as
467 /// a rectangle.
468 #[must_use]
469 pub fn width(&self) -> usize {
470 self.values.iter().map(Vec::len).max().unwrap_or(0)
471 }
472}
473
474impl crate::cli::drive::format::JsonlSerialize for Spreadsheet {
475 fn write_jsonl(&self, out: &mut dyn std::io::Write) -> anyhow::Result<()> {
476 crate::cli::drive::format::write_scalar_jsonl(self, out)
477 }
478}
479
480/// Envelope for `values.batchGet`.
481#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
482pub struct BatchGetValuesResponse {
483 /// One entry per requested range, in the order the server chose — match
484 /// on each entry's own `range`, never on request order.
485 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "valueRanges")]
486 pub value_ranges: Vec<ValueRange>,
487}
488
489/// Response to `values.update`.
490///
491/// The counts are what the request log records as context, so
492/// `omni-dev log --query kind:drivemutation` can answer "what did that write
493/// actually touch" rather than only "a write happened".
494#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
495pub struct UpdateValuesResponse {
496 /// The range that was written, server-normalised.
497 #[serde(
498 default,
499 skip_serializing_if = "Option::is_none",
500 rename = "updatedRange"
501 )]
502 pub updated_range: Option<String>,
503 /// Rows written.
504 #[serde(
505 default,
506 skip_serializing_if = "Option::is_none",
507 rename = "updatedRows"
508 )]
509 pub updated_rows: Option<i64>,
510 /// Columns written.
511 #[serde(
512 default,
513 skip_serializing_if = "Option::is_none",
514 rename = "updatedColumns"
515 )]
516 pub updated_columns: Option<i64>,
517 /// Cells written.
518 #[serde(
519 default,
520 skip_serializing_if = "Option::is_none",
521 rename = "updatedCells"
522 )]
523 pub updated_cells: Option<i64>,
524}
525
526/// Response to `values.append`.
527///
528/// Note the nesting: the counts live under `updates`, not at the top level,
529/// which is the one shape difference from `values.update`.
530#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
531pub struct AppendValuesResponse {
532 /// The table range the append targeted, before the new rows.
533 #[serde(
534 default,
535 skip_serializing_if = "Option::is_none",
536 rename = "tableRange"
537 )]
538 pub table_range: Option<String>,
539 /// What was actually written.
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub updates: Option<UpdateValuesResponse>,
542}
543
544/// Response to `values.clear`.
545#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
546pub struct ClearValuesResponse {
547 /// The range that was cleared, server-normalised.
548 #[serde(
549 default,
550 skip_serializing_if = "Option::is_none",
551 rename = "clearedRange"
552 )]
553 pub cleared_range: Option<String>,
554}
555
556/// One `spreadsheets.batchUpdate` request.
557///
558/// **Externally tagged**, which is exactly Sheets' own wire shape:
559/// `{"addSheet": {...}}`. Every variant here is reachable only through its
560/// owning module's gated engine: the additive verbs behind
561/// `DriveOperation::SheetsStructure` (issue #1613,
562/// [ADR-0075](../../../docs/adrs/adr-0075.md), extended by issue #1643,
563/// [ADR-0078](../../../docs/adrs/adr-0078.md), with formatting, data
564/// validation, `duplicateSheet`, reorder/hide and, since issue #1795
565/// ([ADR-0081](../../../docs/adrs/adr-0081.md) §4), developer-metadata
566/// management, and extended again by issue #1796,
567/// [ADR-0081](../../../docs/adrs/adr-0081.md) §2, with named-range
568/// add/update/delete), the destructive ones behind
569/// `DriveOperation::SheetsDelete` (issue #1623,
570/// [ADR-0077](../../../docs/adrs/adr-0077-sheets-deletion-via-batchupdate.md)),
571/// and the protected-range ones behind `DriveOperation::SheetsProtection`
572/// (issue #1643, [ADR-0078](../../../docs/adrs/adr-0078.md) §2) — there is
573/// still no raw `--requests` passthrough that could construct one of these
574/// outside its gate. `structure.rs`'s `every_delete_verb_gates_on_sheets_delete_not_sheets_structure`/
575/// `every_additive_verb_still_gates_on_sheets_structure` tests pin that every
576/// verb reaches exactly one gate, never the other; `developer_metadata.rs`
577/// has no dispatch table to get wrong in the first place — its `Set`/`Delete`
578/// verbs share the single hardcoded `DriveOperation::SheetsStructure` call
579/// site in `developer_metadata_inner`.
580#[derive(Debug, Clone, Serialize, PartialEq)]
581#[serde(rename_all = "camelCase")]
582pub enum BatchUpdateRequestItem {
583 /// Find and replace cell text (`find-replace`, issue #1841). Gated by
584 /// `DriveOperation::SheetsWrite`; [`FindReplaceRequest::scope`] makes
585 /// an unscoped request unrepresentable.
586 FindReplace(FindReplaceRequest),
587 /// Add a new sheet to the workbook.
588 AddSheet(AddSheetRequest),
589 /// Change an existing sheet's properties — title (rename), index
590 /// (reorder), hidden (hide/show), or, since issue #1835's
591 /// `update-sheet-properties`, any of frozen rows/columns, hidden
592 /// gridlines, tab color, or right-to-left layout. `rename-sheet`/
593 /// `reorder-sheet`/`hide-sheet`/`show-sheet` each set exactly one field
594 /// per request; `update-sheet-properties` may set several at once, but
595 /// the request's `fields` mask always names exactly what was
596 /// populated, one entry per field, so it can never blank a property it
597 /// didn't mean to touch.
598 UpdateSheetProperties(UpdateSheetPropertiesRequest),
599 /// Change workbook-level properties — locale, time zone, auto-recalc
600 /// and/or iterative calculation (`update-workbook-properties`, issue
601 /// #1836). Gated by
602 /// [`crate::drive::write_gate::DriveOperation::SheetsStructure`], like
603 /// [`Self::UpdateSheetProperties`]: no cell's formula changes, only
604 /// (for iterative calculation) what some formulas compute — see
605 /// [`IterativeCalculationSettings`]'s doc comment.
606 UpdateSpreadsheetProperties(UpdateSpreadsheetPropertiesRequest),
607 /// Insert empty rows or columns, shifting existing ones.
608 InsertDimension(InsertDimensionRequest),
609 /// Insert empty cells into a rectangular range, shifting existing cells
610 /// down or right within the grid.
611 InsertRange(InsertRangeRequest),
612 /// Delete an entire sheet from the workbook.
613 DeleteSheet(DeleteSheetRequest),
614 /// Delete whole rows or columns, shifting the remainder to close the gap.
615 DeleteDimension(DeleteDimensionRequest),
616 /// Move whole rows or columns to a new position within their sheet
617 /// (`move-rows`/`move-columns`, issue #1834). Gated by
618 /// [`crate::drive::write_gate::DriveOperation::SheetsStructure`] like
619 /// the inserts, not `SheetsDelete`: a reordering discards nothing
620 /// (ADR-0083's "additive/structural" group).
621 MoveDimension(MoveDimensionRequest),
622 /// Delete a rectangular cell range, shifting the remainder to close the
623 /// gap along one axis.
624 DeleteRange(DeleteRangeRequest),
625 /// Copy an existing sheet within the same workbook.
626 DuplicateSheet(DuplicateSheetRequest),
627 /// Apply a cell format across a range (`format-cells`). Never carries a
628 /// value — see [`RepeatCellData`].
629 RepeatCell(RepeatCellRequest),
630 /// Set border lines on a range's edges (`update-borders`).
631 UpdateBorders(UpdateBordersRequest),
632 /// Merge a range into one cell (`merge-cells`) — the one request this
633 /// enum models that discards data; see `format.rs` module docs.
634 MergeCells(MergeCellsRequest),
635 /// Split a previously merged range back apart (`unmerge-cells`).
636 UnmergeCells(UnmergeCellsRequest),
637 /// Resize rows/columns to fit their content (`auto-resize-dimension`).
638 AutoResizeDimensions(AutoResizeDimensionsRequest),
639 /// Set explicit row height / column width (`update-dimension-properties`).
640 UpdateDimensionProperties(UpdateDimensionPropertiesRequest),
641 /// Set (or, with an absent rule, clear) a data validation rule on a
642 /// range (`set-data-validation` / `clear-data-validation`).
643 SetDataValidation(SetDataValidationRequest),
644 /// Add a protected range (`protect-range`). Gated by
645 /// [`crate::drive::write_gate::DriveOperation::SheetsProtection`], not
646 /// `SheetsStructure` — see that variant's doc comment.
647 AddProtectedRange(AddProtectedRangeRequest),
648 /// Change an existing protected range's description, warning-only flag,
649 /// or editor list (`update-protection`). Same gate as
650 /// [`Self::AddProtectedRange`].
651 UpdateProtectedRange(UpdateProtectedRangeRequest),
652 /// Remove a protected range (`unprotect-range`). Same gate as
653 /// [`Self::AddProtectedRange`] — a permission removal, not a *data*
654 /// deletion, which is why it needs no `SheetsDelete` grant.
655 DeleteProtectedRange(DeleteProtectedRangeRequest),
656 /// Attach a new developer-metadata entry (`set-developer-metadata`,
657 /// when no existing entry matched). Gated by
658 /// [`crate::drive::write_gate::DriveOperation::SheetsStructure`], like
659 /// every other additive verb — see [`DeveloperMetadata`]'s doc comment
660 /// for the DOCUMENT-only guarantee (issue #1795, ADR-0081 §4).
661 CreateDeveloperMetadata(CreateDeveloperMetadataRequest),
662 /// Change an existing developer-metadata entry's value
663 /// (`set-developer-metadata`, when an existing entry matched). Same
664 /// gate as [`Self::CreateDeveloperMetadata`].
665 UpdateDeveloperMetadata(UpdateDeveloperMetadataRequest),
666 /// Remove developer metadata (`delete-developer-metadata`) — a
667 /// third-party add-on's own state, not this spreadsheet's grid data, so
668 /// it stays under `SheetsStructure` rather than `SheetsDelete` (ADR-0081
669 /// §4). `developer_metadata.rs` reads back and reports every entry a
670 /// request like this would remove before ever sending it.
671 DeleteDeveloperMetadata(DeleteDeveloperMetadataRequest),
672 /// Upsert a sheet's basic filter (`set-basic-filter`). Gated by
673 /// `SheetsStructure` (issue #1794, ADR-0081): a filter hides rows,
674 /// which is view state, not data.
675 SetBasicFilter(SetBasicFilterRequest),
676 /// Remove a sheet's basic filter (`clear-basic-filter`). Same gate as
677 /// [`Self::SetBasicFilter`].
678 ClearBasicFilter(ClearBasicFilterRequest),
679 /// Add a named filter view (`add-filter-view`). Same gate as
680 /// [`Self::SetBasicFilter`].
681 AddFilterView(AddFilterViewRequest),
682 /// Change an existing filter view's title, range, sort order, or hidden
683 /// values (`update-filter-view`). Same gate as [`Self::SetBasicFilter`].
684 UpdateFilterView(UpdateFilterViewRequest),
685 /// Remove a filter view (`delete-filter-view`). Same gate as
686 /// [`Self::SetBasicFilter`].
687 DeleteFilterView(DeleteFilterViewRequest),
688 /// Add a conditional format rule (`add-conditional-format`). Gated by
689 /// `DriveOperation::SheetsStructure` — presentational, destroys no data
690 /// (ADR-0081 §1, issue #1793).
691 AddConditionalFormatRule(AddConditionalFormatRuleRequest),
692 /// Replace an existing conditional format rule at an index
693 /// (`update-conditional-format`). Same gate as
694 /// [`Self::AddConditionalFormatRule`].
695 UpdateConditionalFormatRule(UpdateConditionalFormatRuleRequest),
696 /// Remove a conditional format rule at an index
697 /// (`delete-conditional-format`). Same gate as
698 /// [`Self::AddConditionalFormatRule`] — a presentational removal, not a
699 /// *data* deletion.
700 DeleteConditionalFormatRule(DeleteConditionalFormatRuleRequest),
701 /// Add a named range (`add-named-range`). Gated
702 /// `DriveOperation::SheetsStructure` — a label over a region, created
703 /// without touching any cell's value.
704 AddNamedRange(AddNamedRangeRequest),
705 /// Change an existing named range's name and/or range
706 /// (`update-named-range`). Same gate as [`Self::AddNamedRange`].
707 UpdateNamedRange(UpdateNamedRangeRequest),
708 /// Remove a named range (`delete-named-range`). Same gate as
709 /// [`Self::AddNamedRange`] — it removes a label, not grid data; see
710 /// [ADR-0081](../../../docs/adrs/adr-0081.md) §2 for why this stays
711 /// `SheetsStructure` rather than `SheetsDelete`, and for the mandatory
712 /// referencing-formula preview that mitigates the resulting `#REF!`
713 /// errors.
714 DeleteNamedRange(DeleteNamedRangeRequest),
715 /// Add a chart (`add-chart`). Gated by `DriveOperation::SheetsStructure`
716 /// — additive, destroys no data (ADR-0081 §3, issue #1797).
717 AddChart(AddChartRequest),
718 /// Replace an existing chart's spec wholesale (`update-chart`). Same
719 /// gate as [`Self::AddChart`]. Unlike `updateFilterView`, this request
720 /// carries **no field mask** — see [`UpdateChartSpecRequest`]'s doc
721 /// comment.
722 UpdateChartSpec(UpdateChartSpecRequest),
723 /// Add a slicer (`add-slicer`). Same gate as [`Self::AddChart`].
724 AddSlicer(AddSlicerRequest),
725 /// Change an existing slicer's spec (`update-slicer`). Same gate as
726 /// [`Self::AddChart`]. Unlike [`Self::UpdateChartSpec`], this request
727 /// *does* carry a field mask — see [`UpdateSlicerSpecRequest`].
728 UpdateSlicerSpec(UpdateSlicerSpecRequest),
729 /// Remove a chart or slicer (`delete-chart`/`delete-slicer`) — the one
730 /// request shared by both, addressed by `objectId` alone with no
731 /// discriminator naming which kind it is. Gated by
732 /// `DriveOperation::SheetsStructure`, not `SheetsDelete`: ADR-0081 §3
733 /// judges a chart/slicer the same *kind* of removal as
734 /// `unmerge-cells`/`clear-data-validation` — a property of the sheet,
735 /// not the sheet's data — despite being genuinely unrecoverable, which
736 /// is why `embedded_object.rs` reads back and reports the object's
737 /// spec before ever sending this.
738 DeleteEmbeddedObject(DeleteEmbeddedObjectRequest),
739 /// Move and/or resize an existing chart or slicer (`move-chart`/
740 /// `move-slicer`). Gated by `DriveOperation::SheetsStructure` (issue
741 /// #1837, [ADR-0081](../../../docs/adrs/adr-0081.md) §3) — the same
742 /// operation as `add-chart`/`add-slicer`'s own placement, since a move
743 /// discards no data.
744 UpdateEmbeddedObjectPosition(UpdateEmbeddedObjectPositionRequest),
745 /// Set or clear an existing chart's border colour
746 /// (`update-chart-border`). Same gate as
747 /// [`Self::UpdateEmbeddedObjectPosition`] (issue #1837).
748 UpdateEmbeddedObjectBorder(UpdateEmbeddedObjectBorderRequest),
749 /// Write a pivot table into (`add-pivot-table`) or clear one from
750 /// (`delete-pivot-table`) a single anchor cell. The crate's only
751 /// `updateCells` request — issue #1643's "`updateCells` stays unused"
752 /// decision ([`RepeatCellData`]'s doc comment) was specifically about
753 /// writing a *value*, which [`PivotCellData`] still cannot do (it has
754 /// no `userEnteredValue` field, same guarantee as `RepeatCellData`).
755 /// Gated by **both** `DriveOperation::SheetsWrite` and
756 /// `DriveOperation::SheetsStructure` for `add-pivot-table` (a grid
757 /// write with a server-computed extent, but also a named structural
758 /// feature — ADR-0081 §5's union composition), and by `SheetsWrite`
759 /// alone for `delete-pivot-table` (it only ever clears the one anchor
760 /// cell's value, no structural effect).
761 UpdateCells(UpdateCellsRequest),
762 /// Add a new banded range (`add-banding`) — alternating row/column
763 /// colors, presentation applied to a range. Gated by
764 /// `DriveOperation::SheetsStructure` (issue #1832,
765 /// [ADR-0082](../../../docs/adrs/adr-0082-banded-ranges.md)): the same
766 /// "property of the sheet, not the sheet's data" reasoning as
767 /// `unmerge-cells`/`clear-data-validation`.
768 AddBanding(AddBandingRequest),
769 /// Change an existing banded range's colors and/or range
770 /// (`update-banding`). Same gate as [`Self::AddBanding`].
771 UpdateBanding(UpdateBandingRequest),
772 /// Remove a banded range (`delete-banding`). Same gate as
773 /// [`Self::AddBanding`] — it removes presentation, not grid data.
774 DeleteBanding(DeleteBandingRequest),
775 /// Add a new outline group over a row or column span
776 /// (`add-dimension-group`) — the collapsible +/- grouping bar. Gated by
777 /// `DriveOperation::SheetsStructure` (issue #1833,
778 /// [ADR-0084](../../../docs/adrs/adr-0084-dimension-groups.md)): the
779 /// same "property of the sheet, not the sheet's data" reasoning as
780 /// [`Self::AddBanding`].
781 AddDimensionGroup(AddDimensionGroupRequest),
782 /// Change an existing outline group's `collapsed` state
783 /// (`update-dimension-group`). Same gate as [`Self::AddDimensionGroup`].
784 UpdateDimensionGroup(UpdateDimensionGroupRequest),
785 /// Remove an outline group (`delete-dimension-group`). Same gate as
786 /// [`Self::AddDimensionGroup`] — it removes presentation, not grid data.
787 DeleteDimensionGroup(DeleteDimensionGroupRequest),
788 /// Move a source range to a destination, clearing the source
789 /// (`cut-paste`, issue #1839, [ADR-0083](../../../docs/adrs/adr-0083.md)
790 /// §4). Gated by **both** `SheetsWrite` and `SheetsStructure`
791 /// regardless of `pasteType`: the source is always cleared in full —
792 /// values, formats and merges — which is `SheetsStructure`'s territory
793 /// on the formats/merges half exactly as clearing values is
794 /// `SheetsWrite`'s.
795 CutPaste(CutPasteRequest),
796 /// Copy a source range to a destination, optionally repeated
797 /// (`copy-paste`, issue #1839, ADR-0083 §4). Gated by `pasteType`:
798 /// value-only types resolve `SheetsWrite` alone, presentation-only
799 /// types `SheetsStructure` alone, and `PASTE_NORMAL` both.
800 CopyPaste(CopyPasteRequest),
801 /// Paste delimited text into a range as if pasted from the clipboard
802 /// (`paste-data`, issue #1839, ADR-0083 §4). `delimiter`-form only —
803 /// the API's `html` alternative is a documented cut (ADR-0083 §4). Same
804 /// `pasteType` gate mapping as [`Self::CopyPaste`].
805 PasteData(PasteDataRequest),
806 /// Extend a series from source cells into an adjacent destination,
807 /// using Sheets' own pattern-detection heuristics (`auto-fill`, issue
808 /// #1840, [ADR-0083](../../../docs/adrs/adr-0083.md) §1). Gated by
809 /// `DriveOperation::SheetsWrite` alone: it writes ordinary cell
810 /// content into a range, doing nothing a `sheets clear` followed by a
811 /// `sheets write` of the same range could not already do under the
812 /// same grant.
813 AutoFill(AutoFillRequest),
814 /// Reorders the rows of a range using one or more column sort keys
815 /// (`sort-range`, issue #1842). Gated by `SheetsWrite` alone: it is a
816 /// permutation of the named cells' values, never a structural edit.
817 SortRange(SortRangeRequest),
818 /// Shuffles the row order within a bounded range into a
819 /// server-chosen, unpredictable order (`randomize-range`, issue #1845,
820 /// [ADR-0083](../../../docs/adrs/adr-0083.md) §§3, 5, 6). Gated by
821 /// **both** `SheetsWrite` and `SheetsStructure`: live verification
822 /// found a reordered row carries its formatting, notes and
823 /// data-validation rules with it — `SheetsStructure`'s own subject
824 /// matter, exactly as `text-to-columns` was measured doing — and its
825 /// in-range formulas move with their row with their relative
826 /// references rewritten to match.
827 RandomizeRange(RandomizeRangeRequest),
828 /// Splits a single column's delimited text across the adjacent columns
829 /// to its right (`text-to-columns`, issue #1843,
830 /// [ADR-0083](../../../docs/adrs/adr-0083.md) §§1, 5). Gated by
831 /// `DriveOperation::SheetsWrite` **and** `SheetsStructure`: it writes
832 /// ordinary cell content into columns the request derives from
833 /// `source`, which the first covers, and was measured carrying the
834 /// source cell's formatting into those columns, which only the second
835 /// does (§5's live-verification consequence).
836 TextToColumns(TextToColumnsRequest),
837 /// Trims whitespace in every cell of a range (`trim-whitespace`, issue
838 /// #1844, [ADR-0083](../../../docs/adrs/adr-0083.md) §1). Gated by
839 /// `DriveOperation::SheetsWrite` alone: it rewrites ordinary cell
840 /// content in place, doing nothing a `sheets clear` followed by a
841 /// `sheets write` of the same range could not already do under the
842 /// same grant.
843 TrimWhitespace(TrimWhitespaceRequest),
844 /// Removes rows within a range that duplicate an earlier row
845 /// (`delete-duplicates`, issue #1844,
846 /// [ADR-0083](../../../docs/adrs/adr-0083.md) §2). The one verb in
847 /// this tranche gated by
848 /// [`crate::drive::write_gate::DriveOperation::SheetsDelete`] rather
849 /// than `SheetsWrite`: rows cease to exist and the survivors close
850 /// over the gap, which is `delete-range`'s shape, not `clear`'s.
851 DeleteDuplicates(DeleteDuplicatesRequest),
852}
853
854/// Body of `spreadsheets.batchUpdate`.
855#[derive(Debug, Clone, Serialize, PartialEq)]
856pub struct BatchUpdateRequest {
857 /// The requests to apply. Every v1 structural verb sends exactly one,
858 /// which is what makes partial application unobservable.
859 pub requests: Vec<BatchUpdateRequestItem>,
860}
861
862/// `AddSheetRequest` — wraps the new sheet's properties.
863#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
864pub struct AddSheetRequest {
865 /// Properties of the sheet to add.
866 pub properties: NewSheetProperties,
867}
868
869/// The properties of a sheet being added.
870///
871/// Distinct from [`SheetProperties`], which is a *response* type: this one
872/// omits `sheetId` (the server assigns it) and every field is optional
873/// except the title, so an omitted `--rows` inherits Sheets' own default
874/// rather than us inventing one.
875#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
876pub struct NewSheetProperties {
877 /// The new sheet's title.
878 pub title: String,
879 /// Zero-based position in the workbook. Omitted means "append".
880 #[serde(skip_serializing_if = "Option::is_none")]
881 pub index: Option<i64>,
882 /// Initial grid dimensions. Omitted means Sheets' default (1000 x 26).
883 #[serde(skip_serializing_if = "Option::is_none", rename = "gridProperties")]
884 pub grid_properties: Option<GridProperties>,
885}
886
887/// `UpdateSheetPropertiesRequest`.
888///
889/// `fields` is a field mask and is **not** optional: an empty mask is an
890/// error, and a mask naming more than we set would blank the unnamed fields.
891/// Every caller but `update-sheet-properties` (issue #1835) populates
892/// exactly the one [`SheetPropertiesUpdate`] field its `fields` string
893/// names; `update-sheet-properties` may populate several at once (frozen
894/// rows/columns, hidden gridlines, tab color, right-to-left), and its mask
895/// still names exactly what was populated, field for field.
896/// `structure.rs::build_request` is the single place that pairs the
897/// properties and the mask, so they can never drift apart.
898///
899/// `PartialEq`-only, not `Eq`: [`SheetPropertiesUpdate`] embeds
900/// [`ColorStyle`], which wraps [`Color`]'s `f32` channels.
901#[derive(Debug, Clone, Serialize, PartialEq)]
902pub struct UpdateSheetPropertiesRequest {
903 /// The properties to write. `sheet_id` selects the target.
904 pub properties: SheetPropertiesUpdate,
905 /// The field mask limiting what this request may change.
906 pub fields: String,
907}
908
909/// `UpdateSpreadsheetPropertiesRequest` (`update-workbook-properties`, issue
910/// #1836).
911///
912/// Same field-mask discipline as [`UpdateSheetPropertiesRequest`]: `fields`
913/// is not optional, and `structure.rs::build_request` is the single place
914/// that pairs it with [`SpreadsheetPropertiesUpdate`] so the two can never
915/// drift apart. Unlike a sheet-properties update this one has no id to
916/// select a target — the workbook itself is the target, addressed by the
917/// `spreadsheetId` already in the URL.
918#[derive(Debug, Clone, Serialize, PartialEq)]
919pub struct UpdateSpreadsheetPropertiesRequest {
920 /// The properties to write.
921 pub properties: SpreadsheetPropertiesUpdate,
922 /// The field mask limiting what this request may change.
923 pub fields: String,
924}
925
926/// The mutable subset of a workbook's properties this crate can set
927/// (`update-workbook-properties`, issue #1836).
928///
929/// Deliberately not [`SpreadsheetProperties`] itself, for the same reason
930/// [`SheetPropertiesUpdate`] is not [`SheetProperties`]: reusing the response
931/// type would serialise whatever else it happened to carry, and a field mask
932/// widened by accident is how an unintended property gets overwritten. Every
933/// field is independently optional and `structure.rs::build_request` names
934/// in `fields` exactly the ones the caller actually set — never more, since
935/// an unset field named in the mask would blank it rather than leave it
936/// alone.
937///
938/// `iterative_calculation_settings` is `None` both when the caller never
939/// touched iterative calculation (in which case `fields` omits
940/// `iterativeCalculationSettings` and this value is never serialised) and
941/// when the caller explicitly turned it off (in which case `fields` *does*
942/// name it, so the omitted key clears the property) — `structure.rs`'s
943/// `IterativeCalculationToggle` is what tells the two apart before the mask
944/// is built. `PartialEq`-only, not `Eq`, for the same reason
945/// [`IterativeCalculationSettings`] is.
946#[derive(Debug, Clone, Default, Serialize, PartialEq)]
947pub struct SpreadsheetPropertiesUpdate {
948 /// The new locale, for `--locale`.
949 #[serde(skip_serializing_if = "Option::is_none")]
950 pub locale: Option<String>,
951 /// The new time zone, for `--time-zone`.
952 #[serde(skip_serializing_if = "Option::is_none", rename = "timeZone")]
953 pub time_zone: Option<String>,
954 /// The new recalculation interval, for `--auto-recalc`.
955 #[serde(skip_serializing_if = "Option::is_none", rename = "autoRecalc")]
956 pub auto_recalc: Option<RecalculationInterval>,
957 /// The new iterative-calculation settings, for `--iterative-calculation
958 /// on`; `None` to leave it untouched *or* to turn it off — see this
959 /// struct's own doc comment for how `fields` disambiguates the two.
960 #[serde(
961 skip_serializing_if = "Option::is_none",
962 rename = "iterativeCalculationSettings"
963 )]
964 pub iterative_calculation_settings: Option<IterativeCalculationSettings>,
965}
966
967/// Body of `spreadsheets.sheets.copyTo`.
968///
969/// Copies one sheet into another spreadsheet, creating a new sheet there
970/// with a server-assigned id (never the source's), so no destination-side
971/// id collision is possible.
972#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
973pub struct CopySheetToAnotherSpreadsheetRequest {
974 /// The spreadsheet to copy the sheet into.
975 #[serde(rename = "destinationSpreadsheetId")]
976 pub destination_spreadsheet_id: String,
977}
978
979/// The mutable subset of a sheet's properties this crate can set.
980///
981/// Deliberately not [`SheetProperties`]: reusing the response type would
982/// serialise whatever else it happened to carry, and a field mask widened by
983/// accident is how an unintended property gets overwritten.
984///
985/// Every field but `sheet_id` is optional. `title` (rename), `index`
986/// (reorder) and `hidden` (hide/show) each have their own single-field
987/// caller, which sets exactly that field alongside a `fields` mask naming
988/// it. `update-sheet-properties` (issue #1835) is different: it is the
989/// first caller that may populate several fields — `grid_properties`,
990/// `tab_color_style`, `right_to_left` — in one request, each still paired
991/// with its own mask entry. Modelling every settable property on one
992/// struct, rather than one struct per verb, is what lets `resolve_sheet`/
993/// `validate_verb_args`/`build_request`'s existing `updateSheetProperties`
994/// plumbing serve all of them without triplicating it.
995#[derive(Debug, Clone, Default, Serialize, PartialEq)]
996pub struct SheetPropertiesUpdate {
997 /// Which sheet to modify.
998 #[serde(rename = "sheetId")]
999 pub sheet_id: i64,
1000 /// The new title, for a rename.
1001 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub title: Option<String>,
1003 /// The new zero-based position, for a reorder.
1004 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub index: Option<i64>,
1006 /// Whether the sheet should be hidden, for hide/show.
1007 #[serde(skip_serializing_if = "Option::is_none")]
1008 pub hidden: Option<bool>,
1009 /// Frozen rows/columns and hidden gridlines, for `update-sheet-properties`
1010 /// (issue #1835).
1011 #[serde(skip_serializing_if = "Option::is_none", rename = "gridProperties")]
1012 pub grid_properties: Option<GridPropertiesUpdate>,
1013 /// The new tab color, for `update-sheet-properties` (issue #1835).
1014 ///
1015 /// Deliberately write-only: [`SheetProperties`] (the response type)
1016 /// never carries this field, even though the API returns
1017 /// `tabColorStyle` on a sheet that has one. Adding it there would strip
1018 /// `Eq` from `SheetProperties`, and transitively from `Sheet` and
1019 /// `Spreadsheet`, for no benefit — a dry-run preview only ever needs to
1020 /// state what the color *becomes*, never what it was, since (unlike
1021 /// frozen rows/columns) there is no meaningful "before" number to show
1022 /// next to a swatch. `--clear-tab-color` (the CLI layer) leaves this
1023 /// `None` while still naming `tabColorStyle` in the `fields` mask —
1024 /// that is how the API is told to clear a field rather than leave it
1025 /// unset.
1026 #[serde(skip_serializing_if = "Option::is_none", rename = "tabColorStyle")]
1027 pub tab_color_style: Option<ColorStyle>,
1028 /// Whether the sheet should be right-to-left, for `update-sheet-properties`
1029 /// (issue #1835).
1030 #[serde(skip_serializing_if = "Option::is_none", rename = "rightToLeft")]
1031 pub right_to_left: Option<bool>,
1032}
1033
1034/// The `gridProperties` subset [`SheetPropertiesUpdate`] can set — frozen
1035/// rows/columns and hidden gridlines (issue #1835).
1036///
1037/// Mirrors [`SheetPropertiesUpdate`]'s relationship to [`SheetProperties`]:
1038/// a request-side type distinct from the response-side [`GridProperties`]
1039/// so a field mask widened by accident can't blank `rowCount`/`columnCount`,
1040/// which this type has no way to even name.
1041#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1042pub struct GridPropertiesUpdate {
1043 /// Rows to freeze at the top.
1044 #[serde(skip_serializing_if = "Option::is_none", rename = "frozenRowCount")]
1045 pub frozen_row_count: Option<i64>,
1046 /// Columns to freeze at the left.
1047 #[serde(skip_serializing_if = "Option::is_none", rename = "frozenColumnCount")]
1048 pub frozen_column_count: Option<i64>,
1049 /// Whether gridlines should be hidden.
1050 #[serde(skip_serializing_if = "Option::is_none", rename = "hideGridlines")]
1051 pub hide_gridlines: Option<bool>,
1052}
1053
1054/// `DuplicateSheetRequest`.
1055#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1056pub struct DuplicateSheetRequest {
1057 /// The sheet to copy.
1058 #[serde(rename = "sourceSheetId")]
1059 pub source_sheet_id: i64,
1060 /// Where the copy lands. `None` takes Sheets' own default — confirmed
1061 /// against the live API to be the *front* of the workbook (index 0),
1062 /// unlike [`NewSheetProperties::index`], whose `None` really does
1063 /// append.
1064 #[serde(skip_serializing_if = "Option::is_none", rename = "insertSheetIndex")]
1065 pub insert_sheet_index: Option<i64>,
1066 /// The copy's title. `None` takes Sheets' own "Copy of X" default.
1067 #[serde(skip_serializing_if = "Option::is_none", rename = "newSheetName")]
1068 pub new_sheet_name: Option<String>,
1069}
1070
1071/// Which axis a [`DimensionRange`] runs along.
1072#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1073pub enum Dimension {
1074 /// Rows.
1075 #[serde(rename = "ROWS")]
1076 Rows,
1077 /// Columns.
1078 #[serde(rename = "COLUMNS")]
1079 Columns,
1080}
1081
1082impl Dimension {
1083 /// The wire spelling, also used in the request log's `dimension_range`
1084 /// context field and in human-readable output.
1085 #[must_use]
1086 pub fn as_str(self) -> &'static str {
1087 match self {
1088 Self::Rows => "ROWS",
1089 Self::Columns => "COLUMNS",
1090 }
1091 }
1092
1093 /// Singular noun for human-readable output ("row" / "column").
1094 #[must_use]
1095 pub fn noun(self) -> &'static str {
1096 match self {
1097 Self::Rows => "row",
1098 Self::Columns => "column",
1099 }
1100 }
1101
1102 /// The request log's `dimension_range` context value for a **1-based
1103 /// inclusive** span on this axis, e.g. `"ROWS 5:7"`. The one formatter
1104 /// for that key — `structure.rs` (insert/delete-dimension verbs) and
1105 /// `dimension_group.rs` both write it, and neither may spell it
1106 /// differently.
1107 #[must_use]
1108 pub fn span_label(self, start: i64, end: i64) -> String {
1109 format!("{} {start}:{end}", self.as_str())
1110 }
1111}
1112
1113/// A half-open, **zero-based** span of rows or columns.
1114///
1115/// The zero-based half-open shape is the API's, not the CLI's: `--at` is
1116/// 1-based and inclusive, matching A1 and the spreadsheet UI. The single
1117/// conversion lives in `structure.rs::dimension_range`, never at a call
1118/// site.
1119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1120pub struct DimensionRange {
1121 /// The sheet the span belongs to. Proto3 omits this when it's `0` (the
1122 /// first sheet in the workbook), so `#[serde(default)]` is load-bearing,
1123 /// not decorative — see the module doc.
1124 #[serde(default, rename = "sheetId")]
1125 pub sheet_id: i64,
1126 /// Rows or columns.
1127 pub dimension: Dimension,
1128 /// First index, inclusive, zero-based. Omitted on the wire when `0`
1129 /// (row 1 / column A) — see the module doc.
1130 #[serde(default, rename = "startIndex")]
1131 pub start_index: i64,
1132 /// Last index, **exclusive**, zero-based. Omitted on the wire when `0`;
1133 /// defaulted defensively even though a zero-length range is degenerate.
1134 #[serde(default, rename = "endIndex")]
1135 pub end_index: i64,
1136}
1137
1138/// An outline group over a [`DimensionRange`] — the collapsible +/-
1139/// grouping bar (issue #1833,
1140/// [ADR-0084](../../../docs/adrs/adr-0084-dimension-groups.md)).
1141///
1142/// `depth` is server-derived, never client-supplied: `addDimensionGroup`'s
1143/// request carries only `range`, and the server computes the new group's
1144/// depth from its overlap with existing groups on the same axis (a
1145/// superset increments an existing group's depth; a subset creates a new,
1146/// deeper one; a partial overlap widens the existing group to the union
1147/// and creates a new, deeper one over the intersection). This crate does
1148/// not predict that outcome — see the `dimension_group.rs` module doc for
1149/// why.
1150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1151pub struct DimensionGroup {
1152 /// The span this group covers.
1153 pub range: DimensionRange,
1154 /// Nesting level — server-derived, **1-based**: `1` for a group nested
1155 /// in no other, `2` for one whose range lies wholly inside a depth-1
1156 /// group's, and so on (the API reference's own examples number a lone
1157 /// group `depth 1`). `0` never occurs in a reply.
1158 #[serde(default)]
1159 pub depth: i64,
1160 /// Whether the group is collapsed (its member rows/columns hidden).
1161 #[serde(default)]
1162 pub collapsed: bool,
1163}
1164
1165/// A rectangular cell range, addressed numerically.
1166///
1167/// The shape every `batchUpdate` request that targets a *range* (as
1168/// opposed to a whole dimension span like [`DimensionRange`]) uses:
1169/// `repeatCell`, `updateBorders`, `mergeCells`, `unmergeCells`,
1170/// `setDataValidation`, `addProtectedRange`, `deleteRange`.
1171/// `structure.rs::grid_range` is `deleteRange`'s one caller, and always
1172/// sends a fully-bounded value (every field `Some`) — an open-ended delete
1173/// on one axis degenerates into `deleteDimension` semantics and is out of
1174/// scope for it, exactly as [`DeleteDimensionRequest`] already covers.
1175///
1176/// Zero-based and half-open on both axes, matching the API. Each bound is
1177/// independently optional: `None` on both row bounds means "every row" (a
1178/// whole-column reference, `A:A`); `None` on both column bounds means "every
1179/// column" (`5:5`); `None` on all four means the entire sheet. The single
1180/// A1-to-`GridRange` conversion lives in `grid_range::parse_grid_range`,
1181/// never at a call site — see its module docs for why this is new work
1182/// `a1.rs` deliberately doesn't do.
1183///
1184/// Also `Deserialize`, unlike most other outbound-only request pieces in
1185/// this file: `ProtectedRange` (a *response* shape) embeds one, since a
1186/// protected range is itself addressed by the `GridRange` it covers.
1187#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1188pub struct GridRange {
1189 /// The sheet this range belongs to.
1190 #[serde(default, rename = "sheetId")]
1191 pub sheet_id: i64,
1192 /// First row, inclusive, zero-based. `None` means "from the first row".
1193 #[serde(
1194 default,
1195 skip_serializing_if = "Option::is_none",
1196 rename = "startRowIndex"
1197 )]
1198 pub start_row_index: Option<i64>,
1199 /// Last row, **exclusive**, zero-based. `None` means "to the last row".
1200 #[serde(
1201 default,
1202 skip_serializing_if = "Option::is_none",
1203 rename = "endRowIndex"
1204 )]
1205 pub end_row_index: Option<i64>,
1206 /// First column, inclusive, zero-based. `None` means "from the first
1207 /// column".
1208 #[serde(
1209 default,
1210 skip_serializing_if = "Option::is_none",
1211 rename = "startColumnIndex"
1212 )]
1213 pub start_column_index: Option<i64>,
1214 /// Last column, **exclusive**, zero-based. `None` means "to the last
1215 /// column".
1216 #[serde(
1217 default,
1218 skip_serializing_if = "Option::is_none",
1219 rename = "endColumnIndex"
1220 )]
1221 pub end_column_index: Option<i64>,
1222}
1223
1224/// An RGB color, 0.0-1.0 per channel — the Sheets API's own float scale,
1225/// not the 0-255 a hex string like `#RRGGBB` suggests. `format.rs`'s hex
1226/// parser is the one conversion site.
1227///
1228/// Also `Deserialize` (issue #1793): `ConditionalFormatRule` reads existing
1229/// rules back via `list-conditional-formats`, so a color nested inside a
1230/// `BooleanRule`/`GradientRule` must round-trip, unlike `format-cells`'
1231/// purely outbound use of the same type.
1232#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
1233pub struct Color {
1234 /// Red channel, 0.0-1.0. Proto3 omits this on the wire when it's `0.0`
1235 /// — see the module doc.
1236 #[serde(default)]
1237 pub red: f32,
1238 /// Green channel, 0.0-1.0. Omitted on the wire when `0.0`.
1239 #[serde(default)]
1240 pub green: f32,
1241 /// Blue channel, 0.0-1.0. Omitted on the wire when `0.0`.
1242 #[serde(default)]
1243 pub blue: f32,
1244}
1245
1246// `Color` has no meaningful `Eq`: it holds `f32`, so this crate's other
1247// request types (all `derive(PartialEq, Eq)`) can't include it directly
1248// without losing `Eq` themselves — hence the `PartialEq`-only requests
1249// below that embed it.
1250
1251/// `ColorStyle` — the modern wrapper Sheets expects around a plain
1252/// [`Color`].
1253///
1254/// It is a `oneof` on the wire (`rgbColor` xor `themeColor`), modelled here
1255/// as a struct with both arms optional rather than an enum: a serde enum
1256/// (tagged or untagged) rejects `{}` — neither arm set, which `Color`'s own
1257/// all-omitted-on-`0.0` proto3 encoding makes routine — and a struct keeps
1258/// the lossless round-trip in `banding.rs::build_update` simple (clone the
1259/// existing value, touch nothing, re-send it).
1260///
1261/// **This crate only ever *constructs* the `rgbColor` arm** (issue #2020,
1262/// same cut as ADR-0078 §5/0082 §1/0085 §8 — no CLI flag surface for a
1263/// theme color). `themeColor` is read-and-preserved only: a color set
1264/// through the Sheets UI (a built-in alternating-colors preset, a
1265/// theme-palette format) can come back this way, and previously failed the
1266/// whole parse with `missing field rgbColor`. Use [`ColorStyle::rgb`] to
1267/// build one; use [`ColorStyle::describe`] to render one for display.
1268#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1269pub struct ColorStyle {
1270 /// The explicit RGB color.
1271 #[serde(default, skip_serializing_if = "Option::is_none", rename = "rgbColor")]
1272 pub rgb_color: Option<Color>,
1273 /// The name of a theme color slot (e.g. `"ACCENT1"`), read back
1274 /// verbatim and never validated or CLI-settable — see the struct doc.
1275 #[serde(
1276 default,
1277 skip_serializing_if = "Option::is_none",
1278 rename = "themeColor"
1279 )]
1280 pub theme_color: Option<String>,
1281}
1282
1283impl ColorStyle {
1284 /// The one constructor production code uses to build an outbound
1285 /// `ColorStyle` — always the `rgbColor` arm, `themeColor` left unset.
1286 pub fn rgb(color: Color) -> Self {
1287 Self {
1288 rgb_color: Some(color),
1289 theme_color: None,
1290 }
1291 }
1292
1293 /// Builds the `themeColor` arm — test-only, since production code never
1294 /// constructs one (see the struct doc): it exists so a test fixture
1295 /// doesn't have to hand-build the struct literal, which risks setting
1296 /// both arms at once, a state the wire's `oneof` can never produce.
1297 #[cfg(test)]
1298 pub fn theme(name: impl Into<String>) -> Self {
1299 Self {
1300 rgb_color: None,
1301 theme_color: Some(name.into()),
1302 }
1303 }
1304
1305 /// Renders a `ColorStyle` for display: `#RRGGBB` for the `rgbColor`
1306 /// arm, `theme:<NAME>` for `themeColor`, `(unset)` for neither. Prefers
1307 /// `rgb_color` when both are present, which the API treats as a
1308 /// `oneof` and so shouldn't send.
1309 #[must_use]
1310 pub fn describe(&self) -> String {
1311 match (&self.rgb_color, &self.theme_color) {
1312 (Some(rgb), _) => super::format::format_hex_color(*rgb),
1313 (None, Some(name)) => format!("theme:{name}"),
1314 (None, None) => "(unset)".to_string(),
1315 }
1316 }
1317}
1318
1319/// The mutable subset of a cell's text formatting this crate can set —
1320/// `CellFormat.textFormat`.
1321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1322pub struct TextFormat {
1323 /// Bold.
1324 #[serde(skip_serializing_if = "Option::is_none")]
1325 pub bold: Option<bool>,
1326 /// Italic.
1327 #[serde(skip_serializing_if = "Option::is_none")]
1328 pub italic: Option<bool>,
1329 /// Strikethrough.
1330 #[serde(skip_serializing_if = "Option::is_none")]
1331 pub strikethrough: Option<bool>,
1332 /// Underline.
1333 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub underline: Option<bool>,
1335 /// Point size.
1336 #[serde(skip_serializing_if = "Option::is_none", rename = "fontSize")]
1337 pub font_size: Option<i64>,
1338 /// Text color.
1339 #[serde(
1340 skip_serializing_if = "Option::is_none",
1341 rename = "foregroundColorStyle"
1342 )]
1343 pub foreground_color_style: Option<ColorStyle>,
1344 /// Font family name, e.g. `"Arial"`.
1345 #[serde(skip_serializing_if = "Option::is_none", rename = "fontFamily")]
1346 pub font_family: Option<String>,
1347}
1348
1349/// A cell's display number format — `CellFormat.numberFormat`.
1350#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1351pub struct NumberFormat {
1352 /// One of Sheets' type strings (`"TEXT"`, `"NUMBER"`, `"PERCENT"`,
1353 /// `"CURRENCY"`, `"DATE"`, `"TIME"`, `"DATE_TIME"`, `"SCIENTIFIC"`).
1354 /// Defaulted defensively (empty string is not a real Sheets type), the
1355 /// same posture the module doc asks for on every scalar here.
1356 #[serde(default, rename = "type")]
1357 pub format_type: String,
1358 /// The display pattern, e.g. `"#,##0.00"`. Proto3 omits this on the
1359 /// wire when it's `""` (a format with an empty pattern) — see the
1360 /// module doc.
1361 #[serde(default)]
1362 pub pattern: String,
1363}
1364
1365/// `CellFormat.textRotation` — a union.
1366///
1367/// Exactly one of `angle`/`vertical` is ever set, never both: this wire
1368/// shape mirrors the API directly, but the engine only ever builds it from
1369/// `format::TextRotationFlag`, whose two variants make the "both set" state
1370/// unrepresentable one layer up.
1371#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1372pub struct TextRotation {
1373 /// Rotation angle in degrees, -90 to 90.
1374 #[serde(skip_serializing_if = "Option::is_none")]
1375 pub angle: Option<i32>,
1376 /// Whether the text is stacked vertically instead of rotated.
1377 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub vertical: Option<bool>,
1379}
1380
1381/// `CellFormat.padding` — each side is independently optional.
1382#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1383pub struct Padding {
1384 /// Top padding, in pixels.
1385 #[serde(skip_serializing_if = "Option::is_none")]
1386 pub top: Option<i32>,
1387 /// Right padding, in pixels.
1388 #[serde(skip_serializing_if = "Option::is_none")]
1389 pub right: Option<i32>,
1390 /// Bottom padding, in pixels.
1391 #[serde(skip_serializing_if = "Option::is_none")]
1392 pub bottom: Option<i32>,
1393 /// Left padding, in pixels.
1394 #[serde(skip_serializing_if = "Option::is_none")]
1395 pub left: Option<i32>,
1396}
1397
1398/// The mutable subset of `CellFormat` this crate can set via `repeatCell`.
1399///
1400/// Every field is optional and `format.rs::build_fields_mask` names exactly
1401/// the ones populated — the same discipline `SheetPropertiesUpdate` follows
1402/// for `updateSheetProperties`.
1403///
1404/// Also `Deserialize` (issue #1793): `BooleanRule.format` reads an existing
1405/// conditional-format rule's format back via `list-conditional-formats`.
1406#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1407pub struct CellFormat {
1408 /// Text formatting (bold, italic, color, …).
1409 #[serde(skip_serializing_if = "Option::is_none", rename = "textFormat")]
1410 pub text_format: Option<TextFormat>,
1411 /// Cell background color.
1412 #[serde(
1413 skip_serializing_if = "Option::is_none",
1414 rename = "backgroundColorStyle"
1415 )]
1416 pub background_color_style: Option<ColorStyle>,
1417 /// `"LEFT"` / `"CENTER"` / `"RIGHT"`.
1418 #[serde(
1419 skip_serializing_if = "Option::is_none",
1420 rename = "horizontalAlignment"
1421 )]
1422 pub horizontal_alignment: Option<String>,
1423 /// `"TOP"` / `"MIDDLE"` / `"BOTTOM"`.
1424 #[serde(skip_serializing_if = "Option::is_none", rename = "verticalAlignment")]
1425 pub vertical_alignment: Option<String>,
1426 /// Display number format.
1427 #[serde(skip_serializing_if = "Option::is_none", rename = "numberFormat")]
1428 pub number_format: Option<NumberFormat>,
1429 /// `"OVERFLOW_CELL"` / `"CLIP"` / `"WRAP"`.
1430 #[serde(skip_serializing_if = "Option::is_none", rename = "wrapStrategy")]
1431 pub wrap_strategy: Option<String>,
1432 /// Rotation of text in the cell.
1433 #[serde(skip_serializing_if = "Option::is_none", rename = "textRotation")]
1434 pub text_rotation: Option<TextRotation>,
1435 /// `"LINKED"` / `"PLAIN_TEXT"`.
1436 #[serde(
1437 skip_serializing_if = "Option::is_none",
1438 rename = "hyperlinkDisplayType"
1439 )]
1440 pub hyperlink_display_type: Option<String>,
1441 /// Cell padding.
1442 #[serde(skip_serializing_if = "Option::is_none")]
1443 pub padding: Option<Padding>,
1444 /// `"LEFT_TO_RIGHT"` / `"RIGHT_TO_LEFT"`.
1445 #[serde(skip_serializing_if = "Option::is_none", rename = "textDirection")]
1446 pub text_direction: Option<String>,
1447}
1448
1449/// The `cell` payload of a `repeatCellRequest` — deliberately **has no
1450/// `userEnteredValue` field**.
1451///
1452/// That is what makes `format-cells` structurally incapable of writing a
1453/// value: there is nowhere on this type to put one, regardless of what a
1454/// caller asks for (issue #1643's decision that `updateCells` stays
1455/// unused).
1456#[derive(Debug, Clone, Default, Serialize, PartialEq)]
1457pub struct RepeatCellData {
1458 /// The format to apply.
1459 #[serde(rename = "userEnteredFormat")]
1460 pub user_entered_format: CellFormat,
1461}
1462
1463/// `RepeatCellRequest`.
1464#[derive(Debug, Clone, Default, Serialize, PartialEq)]
1465pub struct RepeatCellRequest {
1466 /// Which cells this applies to.
1467 pub range: GridRange,
1468 /// The format to repeat across the range.
1469 pub cell: RepeatCellData,
1470 /// The field mask limiting what this request may change — built from
1471 /// exactly the `CellFormat` fields populated above it, prefixed
1472 /// `userEnteredFormat.` per the API's dotted-path convention.
1473 pub fields: String,
1474}
1475
1476/// One border edge — `Border`.
1477#[derive(Debug, Clone, Default, Serialize, PartialEq)]
1478pub struct Border {
1479 /// `"SOLID"` / `"SOLID_MEDIUM"` / `"SOLID_THICK"` / `"DASHED"` /
1480 /// `"DOTTED"` / `"DOUBLE"`.
1481 pub style: String,
1482 /// The line color.
1483 #[serde(rename = "colorStyle")]
1484 pub color_style: ColorStyle,
1485}
1486
1487/// `UpdateBordersRequest`.
1488///
1489/// Each side is independently optional — an unset side is left exactly as
1490/// it was, which is what lets `format.rs::update-borders` touch only the
1491/// sides `--top`/`--bottom`/`--left`/`--right`/`--all`/
1492/// `--inner-horizontal`/`--inner-vertical` actually named.
1493/// `innerHorizontal`/`innerVertical` are the grid lines *between* cells in
1494/// a multi-cell range, deliberately left out of `--all` since they don't
1495/// apply to a single-cell range the way the four outer edges do.
1496#[derive(Debug, Clone, Default, Serialize, PartialEq)]
1497pub struct UpdateBordersRequest {
1498 /// Which cells this applies to.
1499 pub range: GridRange,
1500 /// Top edge.
1501 #[serde(skip_serializing_if = "Option::is_none")]
1502 pub top: Option<Border>,
1503 /// Bottom edge.
1504 #[serde(skip_serializing_if = "Option::is_none")]
1505 pub bottom: Option<Border>,
1506 /// Left edge.
1507 #[serde(skip_serializing_if = "Option::is_none")]
1508 pub left: Option<Border>,
1509 /// Right edge.
1510 #[serde(skip_serializing_if = "Option::is_none")]
1511 pub right: Option<Border>,
1512 /// Horizontal grid lines between rows within the range.
1513 #[serde(skip_serializing_if = "Option::is_none", rename = "innerHorizontal")]
1514 pub inner_horizontal: Option<Border>,
1515 /// Vertical grid lines between columns within the range.
1516 #[serde(skip_serializing_if = "Option::is_none", rename = "innerVertical")]
1517 pub inner_vertical: Option<Border>,
1518}
1519
1520/// `MergeCellsRequest`.
1521#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1522pub struct MergeCellsRequest {
1523 /// Which cells to merge.
1524 pub range: GridRange,
1525 /// `"MERGE_ALL"` / `"MERGE_COLUMNS"` / `"MERGE_ROWS"`.
1526 #[serde(rename = "mergeType")]
1527 pub merge_type: String,
1528}
1529
1530/// `UnmergeCellsRequest`.
1531#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1532pub struct UnmergeCellsRequest {
1533 /// Which cells to unmerge.
1534 pub range: GridRange,
1535}
1536
1537/// `AutoResizeDimensionsRequest`.
1538#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1539pub struct AutoResizeDimensionsRequest {
1540 /// Which rows or columns to resize.
1541 pub dimensions: DimensionRange,
1542}
1543
1544/// The mutable subset of a dimension's properties this crate can set —
1545/// currently only pixel size.
1546///
1547/// `hiddenByUser` has no flag in v1: sheet-level hide/show
1548/// (`structure.rs::SetSheetVisibility`) already covers the common case, and
1549/// per-row/column hiding is a documented follow-up.
1550#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1551pub struct DimensionProperties {
1552 /// Width (for columns) or height (for rows), in pixels.
1553 #[serde(rename = "pixelSize")]
1554 pub pixel_size: i64,
1555}
1556
1557/// `UpdateDimensionPropertiesRequest`.
1558#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
1559pub struct UpdateDimensionPropertiesRequest {
1560 /// Which rows or columns to modify.
1561 pub range: DimensionRange,
1562 /// The properties to write.
1563 pub properties: DimensionProperties,
1564 /// The field mask limiting what this request may change. Always
1565 /// `"pixelSize"` today — the only field [`DimensionProperties`] models.
1566 pub fields: String,
1567}
1568
1569/// One literal value a [`BooleanCondition`] compares against.
1570///
1571/// Sheets models this as a union: exactly one of `userEnteredValue` (a
1572/// literal string, parsed against the cell's type at evaluation time) or
1573/// `relativeDate` (one of Sheets' `RelativeDate` enum strings, used by the
1574/// date conditions' relative forms) is ever present.
1575#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1576pub struct ConditionValue {
1577 /// A literal value, present unless `relative_date` is.
1578 #[serde(
1579 default,
1580 skip_serializing_if = "Option::is_none",
1581 rename = "userEnteredValue"
1582 )]
1583 pub user_entered_value: Option<String>,
1584 /// A `RelativeDate` enum string (`"TODAY"`, `"PAST_WEEK"`, …), present
1585 /// unless `user_entered_value` is.
1586 #[serde(
1587 default,
1588 skip_serializing_if = "Option::is_none",
1589 rename = "relativeDate"
1590 )]
1591 pub relative_date: Option<String>,
1592}
1593
1594/// A data validation rule's condition — `DataValidationRule.condition`.
1595///
1596/// Only the handful of `type`s `validation.rs` builds are ever constructed
1597/// here (`ONE_OF_LIST`, `ONE_OF_RANGE`, `NUMBER_BETWEEN`/`NUMBER_NOT_BETWEEN`,
1598/// the numeric comparators, `TEXT_*`, `DATE_*`, `BLANK`/`NOT_BLANK`,
1599/// `BOOLEAN`, `CUSTOM_FORMULA`), but the type itself is a plain string so a
1600/// rule this crate didn't create (read back via `list-protections`' wider
1601/// fields mask, or a workbook edited outside this tool) still round-trips
1602/// rather than failing to parse.
1603#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1604pub struct BooleanCondition {
1605 /// Sheets' condition type string.
1606 #[serde(default, rename = "type")]
1607 pub condition_type: String,
1608 /// The condition's operands, when it has any (`BOOLEAN` with no
1609 /// argument has none).
1610 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1611 pub values: Vec<ConditionValue>,
1612}
1613
1614/// `DataValidationRule`.
1615#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1616pub struct DataValidationRule {
1617 /// What the entered value is checked against.
1618 #[serde(default)]
1619 pub condition: BooleanCondition,
1620 /// Shown as a tooltip on the cell.
1621 #[serde(
1622 default,
1623 skip_serializing_if = "Option::is_none",
1624 rename = "inputMessage"
1625 )]
1626 pub input_message: Option<String>,
1627 /// `true` rejects an invalid entry outright; `false`/absent shows a
1628 /// warning but allows it. Mirrors the API's own default (`false`) when
1629 /// omitted, so `set-data-validation`'s default (`--show-warning` unset)
1630 /// sends an explicit `true` rather than relying on that default holding.
1631 #[serde(default, skip_serializing_if = "Option::is_none")]
1632 pub strict: Option<bool>,
1633}
1634
1635/// `SetDataValidationRequest`.
1636///
1637/// `rule: None` **clears** the range's validation — the API's own
1638/// documented meaning for an absent `rule`, which is exactly what
1639/// `clear-data-validation` sends and why [`Self::rule`] is
1640/// `skip_serializing_if`-omitted rather than sent as an explicit `null`.
1641#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1642pub struct SetDataValidationRequest {
1643 /// Which cells this applies to.
1644 pub range: GridRange,
1645 /// The rule to set, or `None` to clear.
1646 #[serde(skip_serializing_if = "Option::is_none")]
1647 pub rule: Option<DataValidationRule>,
1648}
1649
1650/// The editors of a protected range — `Editors`. Only the explicit
1651/// per-user list is modelled; Sheets' `domainUsersCanEdit` and `groups`
1652/// have no flag in v1.
1653#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1654pub struct ProtectedRangeEditors {
1655 /// Email addresses granted an exemption from the protection.
1656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1657 pub users: Vec<String>,
1658}
1659
1660/// A protected range, as `addProtectedRange` builds one and
1661/// `spreadsheets.get` (with the wider protections `fields` mask) reads one
1662/// back.
1663#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1664pub struct ProtectedRange {
1665 /// The server-assigned stable id. Absent on a request this crate is
1666 /// building (the server assigns it); always present on one read back.
1667 #[serde(
1668 default,
1669 skip_serializing_if = "Option::is_none",
1670 rename = "protectedRangeId"
1671 )]
1672 pub protected_range_id: Option<i64>,
1673 /// The protected cells. Every bound `None` protects the whole sheet
1674 /// (`protect-range --whole-sheet`).
1675 #[serde(default, skip_serializing_if = "Option::is_none")]
1676 pub range: Option<GridRange>,
1677 /// A human-readable note about why the range is protected.
1678 #[serde(default, skip_serializing_if = "Option::is_none")]
1679 pub description: Option<String>,
1680 /// `true` blocks an edit outright; `false`/absent only warns.
1681 #[serde(
1682 default,
1683 skip_serializing_if = "Option::is_none",
1684 rename = "warningOnly"
1685 )]
1686 pub warning_only: Option<bool>,
1687 /// Who may edit the protected cells despite the protection.
1688 #[serde(default, skip_serializing_if = "Option::is_none")]
1689 pub editors: Option<ProtectedRangeEditors>,
1690}
1691
1692/// `AddProtectedRangeRequest`.
1693#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1694pub struct AddProtectedRangeRequest {
1695 /// The range to protect. `protected_range_id` is left unset — the
1696 /// server assigns it, only knowable from the reply.
1697 #[serde(rename = "protectedRange")]
1698 pub protected_range: ProtectedRange,
1699}
1700
1701/// The mutable subset of a protected range `update-protection` can change.
1702///
1703/// Deliberately not [`ProtectedRange`]: reusing the response type would let
1704/// a caller accidentally attempt to overwrite `range`, which Sheets does
1705/// not allow this request to change at all — resizing a protection means
1706/// deleting and re-adding it, not updating.
1707#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1708pub struct ProtectedRangeUpdate {
1709 /// Which protected range to modify.
1710 #[serde(rename = "protectedRangeId")]
1711 pub protected_range_id: i64,
1712 /// The new description, when changing it.
1713 #[serde(skip_serializing_if = "Option::is_none")]
1714 pub description: Option<String>,
1715 /// The new warning-only flag, when changing it.
1716 #[serde(skip_serializing_if = "Option::is_none", rename = "warningOnly")]
1717 pub warning_only: Option<bool>,
1718 /// The new, complete editor list, when changing it. Sheets has no
1719 /// incremental add/remove — `protection.rs` computes this as the full
1720 /// resulting set from the range's current editors plus `--add-editor`/
1721 /// minus `--remove-editor`.
1722 #[serde(skip_serializing_if = "Option::is_none")]
1723 pub editors: Option<ProtectedRangeEditors>,
1724}
1725
1726/// `UpdateProtectedRangeRequest`.
1727#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1728pub struct UpdateProtectedRangeRequest {
1729 /// The properties to write. `protected_range_id` selects the target.
1730 #[serde(rename = "protectedRange")]
1731 pub protected_range: ProtectedRangeUpdate,
1732 /// The field mask limiting what this request may change.
1733 pub fields: String,
1734}
1735
1736/// `DeleteProtectedRangeRequest`.
1737#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
1738pub struct DeleteProtectedRangeRequest {
1739 /// Which protected range to remove.
1740 #[serde(rename = "protectedRangeId")]
1741 pub protected_range_id: i64,
1742}
1743
1744/// One column's filter criteria within a [`BasicFilter`] or [`FilterView`]
1745/// (issue #1794).
1746///
1747/// **`hiddenValues` only** — the literal "uncheck a value in the dropdown"
1748/// filter, which is by far the most common real use. Sheets' full
1749/// `FilterCriteria` also supports a `condition` field carrying the same
1750/// `BooleanCondition` vocabulary `validation.rs` curates for data
1751/// validation; that is a documented cut for this issue, not a silent gap —
1752/// see `filter.rs`'s module docs.
1753///
1754/// The unmodelled fields a column read back from Sheets may carry
1755/// (`condition`, `visibleBackgroundColorStyle`, …) are kept in
1756/// [`Self::extra`] rather than dropped, so a filter view `update-filter-view`
1757/// re-creates wholesale (issue #1931) keeps them.
1758#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1759pub struct FilterCriteria {
1760 /// Values to hide in this column.
1761 #[serde(
1762 default,
1763 skip_serializing_if = "Vec::is_empty",
1764 rename = "hiddenValues"
1765 )]
1766 pub hidden_values: Vec<String>,
1767 /// Every other `FilterCriteria` field this crate doesn't model,
1768 /// preserved verbatim across a read-then-write cycle — the same
1769 /// `flatten` technique as [`ChartSpec::extra`].
1770 #[serde(flatten)]
1771 pub extra: BTreeMap<String, serde_json::Value>,
1772}
1773
1774impl FilterCriteria {
1775 /// A criterion hiding exactly `hidden_values`, with nothing unmodelled.
1776 #[must_use]
1777 pub fn hiding(hidden_values: Vec<String>) -> Self {
1778 Self {
1779 hidden_values,
1780 extra: BTreeMap::new(),
1781 }
1782 }
1783
1784 /// A criterion that hides nothing: no hidden values and no unmodelled
1785 /// condition. This is what Sheets leaves behind for a column whose
1786 /// criteria were reset with `{}` — it keeps the column key, so a read
1787 /// back returns `"1": {}` rather than dropping the entry (issue #1931).
1788 #[must_use]
1789 pub fn is_empty(&self) -> bool {
1790 self.hidden_values.is_empty() && self.extra.is_empty()
1791 }
1792}
1793
1794/// Sort direction within a [`SortSpec`].
1795#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1796#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1797pub enum SortOrder {
1798 /// Low to high.
1799 Ascending,
1800 /// High to low.
1801 Descending,
1802}
1803
1804/// One column's sort order within a [`BasicFilter`] or [`FilterView`].
1805#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1806pub struct SortSpec {
1807 /// The 0-based column index within the sheet — absolute, not relative
1808 /// to the filter's own range. Proto3 omits this on the wire when it's
1809 /// `0` (a sort on column A) — see the module doc. `Sheet` embeds the
1810 /// basic filter, so this reaches **every** metadata read, not just the
1811 /// filter verbs.
1812 #[serde(default, rename = "dimensionIndex")]
1813 pub dimension_index: i64,
1814 /// The direction to sort that column.
1815 #[serde(rename = "sortOrder")]
1816 pub sort_order: SortOrder,
1817}
1818
1819/// A sheet's basic filter.
1820///
1821/// As `setBasicFilter` builds one and `spreadsheets.get` (with the wider
1822/// filter-views `fields` mask) reads one back. A sheet has at most one, so
1823/// `set-basic-filter` is an upsert and `clear-basic-filter` needs only a
1824/// `sheetId` (see [`ClearBasicFilterRequest`]) — issue #1794.
1825#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1826pub struct BasicFilter {
1827 /// The filtered range.
1828 #[serde(default, skip_serializing_if = "Option::is_none")]
1829 pub range: Option<GridRange>,
1830 /// Sort order applied on top of the filter, in priority order.
1831 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "sortSpecs")]
1832 pub sort_specs: Vec<SortSpec>,
1833 /// Per-column criteria, keyed by the 0-based column index as a string
1834 /// (the wire format's own map key shape). `BTreeMap`, not `HashMap`, so
1835 /// serialization is deterministic.
1836 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1837 pub criteria: BTreeMap<String, FilterCriteria>,
1838}
1839
1840/// `SetBasicFilterRequest` — upserts the sheet's basic filter, replacing any
1841/// existing one wholesale (issue #1794).
1842#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1843pub struct SetBasicFilterRequest {
1844 /// The filter to set. `range.sheet_id` selects the target sheet.
1845 pub filter: BasicFilter,
1846}
1847
1848/// `ClearBasicFilterRequest` — a basic filter needs no identifier beyond the
1849/// sheet, since a sheet has at most one (issue #1794).
1850#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
1851pub struct ClearBasicFilterRequest {
1852 /// The sheet to clear the basic filter from.
1853 #[serde(rename = "sheetId")]
1854 pub sheet_id: i64,
1855}
1856
1857/// A named, id-addressed filter view.
1858///
1859/// As `addFilterView`/`updateFilterView` build one and `spreadsheets.get`
1860/// (with the wider filter-views `fields` mask) reads one back (issue
1861/// #1794). Unlike the basic filter, a sheet may have many;
1862/// `filter_view_id` is the stable handle `list-filter-views` discovers.
1863#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1864pub struct FilterView {
1865 /// The server-assigned stable id. Absent on an `add-filter-view`
1866 /// request this crate is building; always present on one read back.
1867 /// `update-filter-view` also sets it on the `addFilterView` half of
1868 /// the delete-and-re-add it uses to remove sort columns, which Sheets
1869 /// honours, so the view keeps its id (issue #1931).
1870 #[serde(
1871 default,
1872 skip_serializing_if = "Option::is_none",
1873 rename = "filterViewId"
1874 )]
1875 pub filter_view_id: Option<i64>,
1876 /// A human-readable name for the view.
1877 #[serde(default, skip_serializing_if = "Option::is_none")]
1878 pub title: Option<String>,
1879 /// The filtered range. Sheets reports the resolved range even for a
1880 /// view bound to a table (see [`Self::table_id`]), but rejects an
1881 /// `addFilterView` that sets both.
1882 #[serde(default, skip_serializing_if = "Option::is_none")]
1883 pub range: Option<GridRange>,
1884 /// The named range the view is bound to, when it is. Never set by an
1885 /// `add-filter-view` this crate builds. Live testing found Sheets
1886 /// accepts it on `addFilterView` but does not report it back (only the
1887 /// resolved `range`), so it is modelled for completeness: read back,
1888 /// it is carried over wherever [`Self::table_id`] is (issue #1931).
1889 #[serde(
1890 default,
1891 skip_serializing_if = "Option::is_none",
1892 rename = "namedRangeId"
1893 )]
1894 pub named_range_id: Option<String>,
1895 /// The table the view is bound to, when it is. Never set by an
1896 /// `add-filter-view` this crate builds; read back so that
1897 /// `update-filter-view`'s re-creation re-binds the view to its table
1898 /// instead of pinning it to the table's current range (issue #1931).
1899 #[serde(default, skip_serializing_if = "Option::is_none", rename = "tableId")]
1900 pub table_id: Option<String>,
1901 /// Sort order applied on top of the filter, in priority order.
1902 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "sortSpecs")]
1903 pub sort_specs: Vec<SortSpec>,
1904 /// Per-column criteria, keyed by the 0-based column index as a string.
1905 /// See [`BasicFilter::criteria`].
1906 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1907 pub criteria: BTreeMap<String, FilterCriteria>,
1908}
1909
1910/// `AddFilterViewRequest`.
1911#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1912pub struct AddFilterViewRequest {
1913 /// The view to create. `filter_view_id` is left unset — the server
1914 /// assigns it, only knowable from the reply.
1915 pub filter: FilterView,
1916}
1917
1918/// The body of an `updateFilterView` request: every field but the id is
1919/// optional, present exactly when [`UpdateFilterViewRequest::fields`] names
1920/// it (issue #1931).
1921///
1922/// Deliberately not [`FilterView`]: that type skips an *empty*
1923/// `sortSpecs`/`criteria` on the wire, which is right for an add or a
1924/// read-back but let an update name a field in its mask while sending no
1925/// value for it. Here presence is an `Option`, so a masked field is always
1926/// in the body, even when empty.
1927///
1928/// **Sheets merges these, it never replaces them** (live-verified for issue
1929/// #1931): the sent `sortSpecs` go first, followed by every existing entry
1930/// for a column not sent; `criteria` merges per column; an empty
1931/// `sortSpecs: []`/`criteria: {}` changes nothing, even with
1932/// `fields: "*"`. So `filter.rs::build_update` sends only what that merge
1933/// turns into the intended result, and removes a sort column by
1934/// re-creating the view instead.
1935#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1936pub struct FilterViewUpdate {
1937 /// Which filter view to change.
1938 #[serde(rename = "filterViewId")]
1939 pub filter_view_id: i64,
1940 /// The new title, when changing it.
1941 #[serde(skip_serializing_if = "Option::is_none")]
1942 pub title: Option<String>,
1943 /// The new filtered range, when changing it.
1944 #[serde(skip_serializing_if = "Option::is_none")]
1945 pub range: Option<GridRange>,
1946 /// The complete resulting sort order, when changing it. It always
1947 /// names every column the view already sorts by, so the server's merge
1948 /// yields exactly this list.
1949 #[serde(skip_serializing_if = "Option::is_none", rename = "sortSpecs")]
1950 pub sort_specs: Option<Vec<SortSpec>>,
1951 /// The criteria columns to change, when changing any: each one replaces
1952 /// that column's criteria wholesale, and a column absent here is left
1953 /// alone. `{}` resets a column (see [`FilterCriteria::is_empty`]).
1954 #[serde(skip_serializing_if = "Option::is_none")]
1955 pub criteria: Option<BTreeMap<String, FilterCriteria>>,
1956}
1957
1958/// `UpdateFilterViewRequest`.
1959#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
1960pub struct UpdateFilterViewRequest {
1961 /// The properties to write. `filter_view_id` selects the target.
1962 pub filter: FilterViewUpdate,
1963 /// The field mask limiting what this request may change. Names exactly
1964 /// the fields [`FilterViewUpdate`] carries a value for.
1965 pub fields: String,
1966}
1967
1968/// `DeleteFilterViewRequest`.
1969#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
1970pub struct DeleteFilterViewRequest {
1971 /// Which filter view to remove.
1972 #[serde(rename = "filterId")]
1973 pub filter_id: i64,
1974}
1975
1976/// A conditional format rule — one entry of a [`Sheet`]'s
1977/// `conditional_formats` (issue #1793).
1978///
1979/// Index-addressed within that list, not id-addressed like
1980/// [`ProtectedRange`]: `add-conditional-format`/`update-conditional-format`/
1981/// `delete-conditional-format`'s `--index` refers to this rule's ordinal
1982/// position, which shifts when an earlier-indexed rule is deleted —
1983/// `list-conditional-formats` exists to make that position discoverable
1984/// immediately before acting on it.
1985///
1986/// Exactly one of `boolean_rule`/`gradient_rule` is ever present, mirroring
1987/// the API's own union; this crate never constructs both, but tolerates
1988/// reading back a rule with neither modelled field set if a future Sheets
1989/// rule type this crate doesn't build is present.
1990#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1991pub struct ConditionalFormatRule {
1992 /// The range(s) this rule applies to.
1993 pub ranges: Vec<GridRange>,
1994 /// A condition-triggered format.
1995 #[serde(
1996 default,
1997 skip_serializing_if = "Option::is_none",
1998 rename = "booleanRule"
1999 )]
2000 pub boolean_rule: Option<BooleanRule>,
2001 /// A color-scale format.
2002 #[serde(
2003 default,
2004 skip_serializing_if = "Option::is_none",
2005 rename = "gradientRule"
2006 )]
2007 pub gradient_rule: Option<GradientRule>,
2008}
2009
2010/// `BooleanRule` — applies `format` to every cell in the rule's ranges for
2011/// which `condition` evaluates true.
2012#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2013pub struct BooleanRule {
2014 /// What triggers the format. The same wire shape as
2015 /// [`DataValidationRule::condition`], but built from
2016 /// `conditional_format::FormatCondition` rather than `validation::Condition`
2017 /// — the two surfaces support an overlapping but different set of
2018 /// condition types (see `conditional_format.rs`'s module doc).
2019 pub condition: BooleanCondition,
2020 /// The format to apply when `condition` is true. Reused verbatim from
2021 /// `format-cells`/`format.rs`.
2022 pub format: CellFormat,
2023}
2024
2025/// `GradientRule` — a color scale of two or three [`InterpolationPoint`]s.
2026/// `midpoint` absent means a plain two-color scale.
2027///
2028/// A documented cut (issue #1793), mirroring `validation.rs`'s "curate
2029/// rather than chase every enum value" stance: the endpoints this crate
2030/// builds are always anchored `MIN`/`MAX`. Sheets also allows an endpoint to
2031/// be anchored at an explicit `NUMBER`/`PERCENT`/`PERCENTILE` value, which
2032/// this crate doesn't build — `docs/drive.md` names this gap — but reads
2033/// back from a rule made in the Sheets UI.
2034///
2035/// Every point is optional, as in the API: this crate always sets
2036/// `minpoint`/`maxpoint` when building a rule, and never sends back a rule
2037/// it read, so the optionality only makes reads tolerant (issue #1944).
2038#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2039pub struct GradientRule {
2040 /// The low end of the scale.
2041 #[serde(default, skip_serializing_if = "Option::is_none")]
2042 pub minpoint: Option<InterpolationPoint>,
2043 /// An optional midpoint.
2044 #[serde(default, skip_serializing_if = "Option::is_none")]
2045 pub midpoint: Option<InterpolationPoint>,
2046 /// The high end of the scale.
2047 #[serde(default, skip_serializing_if = "Option::is_none")]
2048 pub maxpoint: Option<InterpolationPoint>,
2049}
2050
2051/// One color/anchor pair of a [`GradientRule`] — `InterpolationPoint`.
2052#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2053pub struct InterpolationPoint {
2054 /// The color at this point. The API's deprecated plain `color` field,
2055 /// which Sheets still echoes back alongside it, is ignored.
2056 #[serde(rename = "colorStyle")]
2057 pub color_style: ColorStyle,
2058 /// One of Sheets' `MIN`/`MAX`/`NUMBER`/`PERCENT`/`PERCENTILE`. A plain
2059 /// string, the same tolerate-unmodelled stance as
2060 /// [`BooleanCondition::condition_type`] — a rule read back with a type
2061 /// this crate doesn't build still round-trips. Defaulted defensively
2062 /// like that field — see the module doc on proto3 zero-value omission
2063 /// (issue #1929).
2064 #[serde(default, rename = "type")]
2065 pub point_type: String,
2066 /// The threshold value, untouched — Sheets parses it at evaluation
2067 /// time, the same trust-the-caller stance the rest of this module
2068 /// takes. Absent for a `MIN`/`MAX` point, which has none.
2069 #[serde(default, skip_serializing_if = "Option::is_none")]
2070 pub value: Option<String>,
2071}
2072
2073/// `AddConditionalFormatRuleRequest`.
2074#[derive(Debug, Clone, Serialize, PartialEq)]
2075pub struct AddConditionalFormatRuleRequest {
2076 /// The rule to add.
2077 pub rule: ConditionalFormatRule,
2078 /// Where to insert it — the sheet's current `conditional_formats.len()`
2079 /// appends.
2080 pub index: i64,
2081}
2082
2083/// `UpdateConditionalFormatRuleRequest`.
2084///
2085/// Only the "replace the rule at this index" form is modelled — Sheets also
2086/// supports moving a rule from `index` to a `newIndex` without changing its
2087/// content, which this crate doesn't build (a documented cut: the issue
2088/// asks for position-addressed update/delete, not reordering).
2089#[derive(Debug, Clone, Serialize, PartialEq)]
2090pub struct UpdateConditionalFormatRuleRequest {
2091 /// The rule's new content.
2092 pub rule: ConditionalFormatRule,
2093 /// Which rule, by ordinal position, to replace.
2094 pub index: i64,
2095}
2096
2097/// `DeleteConditionalFormatRuleRequest`.
2098#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
2099pub struct DeleteConditionalFormatRuleRequest {
2100 /// Which sheet the rule belongs to.
2101 #[serde(rename = "sheetId")]
2102 pub sheet_id: i64,
2103 /// Which rule, by ordinal position, to remove.
2104 pub index: i64,
2105}
2106
2107/// A named range, as `addNamedRange` builds one and `spreadsheets.get` (with
2108/// the wider named-ranges `fields` mask) reads one back.
2109///
2110/// Unlike [`ProtectedRange::protected_range_id`] (an `i64`), Sheets assigns
2111/// `namedRangeId` as a **string** — don't copy the protected-range id type
2112/// here.
2113#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2114pub struct NamedRange {
2115 /// The server-assigned stable id. Absent on a request this crate is
2116 /// building (the server assigns it); always present on one read back.
2117 #[serde(
2118 default,
2119 skip_serializing_if = "Option::is_none",
2120 rename = "namedRangeId"
2121 )]
2122 pub named_range_id: Option<String>,
2123 /// The name, unique workbook-wide.
2124 #[serde(default)]
2125 pub name: String,
2126 /// The range the name refers to. Always present — unlike
2127 /// [`ProtectedRange::range`], a named range has no "whole workbook"
2128 /// case, though `range` with every bound unset still covers a whole
2129 /// sheet.
2130 #[serde(default)]
2131 pub range: GridRange,
2132}
2133
2134/// `AddNamedRangeRequest`.
2135#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
2136pub struct AddNamedRangeRequest {
2137 /// The name and range to add. `named_range_id` is left unset — the
2138 /// server assigns it, only knowable from the reply.
2139 #[serde(rename = "namedRange")]
2140 pub named_range: NamedRange,
2141}
2142
2143/// `UpdateNamedRangeRequest`.
2144///
2145/// Reuses [`NamedRange`] directly, unlike `update-protection`'s
2146/// [`ProtectedRangeUpdate`]: a named range has no field `update-named-range`
2147/// must exclude — both `name` and `range` are freely re-settable, so there
2148/// is no risk of a caller accidentally attempting to change something the
2149/// API forbids changing this way.
2150#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
2151pub struct UpdateNamedRangeRequest {
2152 /// The properties to write. `named_range_id` selects the target.
2153 #[serde(rename = "namedRange")]
2154 pub named_range: NamedRange,
2155 /// The field mask limiting what this request may change.
2156 pub fields: String,
2157}
2158
2159/// `DeleteNamedRangeRequest`.
2160#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2161pub struct DeleteNamedRangeRequest {
2162 /// Which named range to remove.
2163 #[serde(rename = "namedRangeId")]
2164 pub named_range_id: String,
2165}
2166
2167// ── Charts and slicers (issue #1797) ────────────────────────────────────
2168//
2169// `ChartSpec` is the largest union in the Sheets API — roughly 15 chart
2170// types, each with its own nested spec. This crate models only what it
2171// builds or needs to read back to merge an update: `basicChart` (the
2172// COLUMN/BAR/LINE/AREA/SCATTER subset `embedded_object.rs` supports) and
2173// `pieChart`. Every other named field of `ChartSpec`/`BasicChartSpec`/
2174// `BasicChartAxis`/`BasicChartSeries`/`PieChartSpec` — `titleTextFormat`,
2175// `backgroundColor`, `hiddenDimensionStrategy`, per-series `styleOverrides`,
2176// the other 13 chart-type variants (`bubbleChart`, `histogramChart`, …) —
2177// is **not modelled as a named field**. Instead each of these types carries
2178// `#[serde(flatten)] extra: BTreeMap<String, serde_json::Value>`, which does
2179// two jobs at once: reading an existing chart preserves every field this
2180// crate doesn't touch losslessly (essential for `update-chart`, which sends
2181// a *whole* replacement spec — see [`UpdateChartSpecRequest`]), and finding
2182// a key like `"histogramChart"` in `ChartSpec::extra` is exactly how
2183// `embedded_object.rs::merge_chart_spec` detects an unsupported existing
2184// chart and refuses rather than silently discarding it.
2185//
2186// `BTreeMap`, not `HashMap`, for the same determinism reason `jev`'s wire
2187// types use it (STYLE, `src/jev/protocol.rs`).
2188//
2189// `serde_json::Value` itself derives `Eq` (unlike an `f64` on its own), so
2190// most of these types derive `Eq` too. The exception is `PieChartSpec`
2191// (its `pie_hole` is a plain `f64`) and everything that transitively embeds
2192// it — `ChartSpec`, `EmbeddedChart`, and the requests/replies built from
2193// them — which stay `PartialEq`-only, the same reason [`Color`] forces
2194// `PartialEq`-only up through everything that embeds *it*.
2195
2196/// A chart's border — `EmbeddedObjectBorder` (issue #1837).
2197///
2198/// Only `colorStyle` is modelled; the API's deprecated plain `color` is
2199/// never sent, the same cut `banding.rs`/`format.rs` make. Unlike a cell
2200/// [`Border`], this type has **no style and no width** — a chart border is
2201/// a colour and nothing else. A slicer has no border field at all.
2202#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2203pub struct EmbeddedObjectBorder {
2204 /// The border's colour. Absent means no border (or, on
2205 /// `update-chart-border --clear`, an explicit reset to none).
2206 #[serde(
2207 default,
2208 skip_serializing_if = "Option::is_none",
2209 rename = "colorStyle"
2210 )]
2211 pub color_style: Option<ColorStyle>,
2212}
2213
2214/// One chart embedded on a sheet — `EmbeddedChart`.
2215#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2216pub struct EmbeddedChart {
2217 /// The server-assigned stable id. Absent on an `add-chart` request this
2218 /// crate is building; always present on one read back or on an
2219 /// `update-chart`/`delete-chart` request.
2220 #[serde(default, skip_serializing_if = "Option::is_none", rename = "chartId")]
2221 pub chart_id: Option<i64>,
2222 /// The chart's content and styling.
2223 #[serde(default, skip_serializing_if = "Option::is_none")]
2224 pub spec: Option<ChartSpec>,
2225 /// Where the chart is anchored.
2226 #[serde(default, skip_serializing_if = "Option::is_none")]
2227 pub position: Option<EmbeddedObjectPosition>,
2228 /// The chart's border colour, if it has one. Read back for free once
2229 /// requested — `SPREADSHEET_FIELDS_WITH_EMBEDDED_OBJECTS` already
2230 /// requests `sheets.charts` wholesale — so `update-chart-border`'s
2231 /// preview can report the colour about to be replaced (issue #1837).
2232 #[serde(default, skip_serializing_if = "Option::is_none")]
2233 pub border: Option<EmbeddedObjectBorder>,
2234}
2235
2236/// A chart's content and styling — `ChartSpec`. See the module-level note
2237/// above for which chart types this crate models and why unmodelled fields
2238/// round-trip through `extra`.
2239#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2240pub struct ChartSpec {
2241 /// The chart's title.
2242 #[serde(default, skip_serializing_if = "Option::is_none")]
2243 pub title: Option<String>,
2244 /// The chart's subtitle.
2245 #[serde(default, skip_serializing_if = "Option::is_none")]
2246 pub subtitle: Option<String>,
2247 /// A basic (column/bar/line/area/scatter) chart's configuration.
2248 /// Mutually exclusive with [`Self::pie_chart`] and with every
2249 /// unmodelled chart-type field that may be present in `extra`.
2250 #[serde(
2251 default,
2252 skip_serializing_if = "Option::is_none",
2253 rename = "basicChart"
2254 )]
2255 pub basic_chart: Option<BasicChartSpec>,
2256 /// A pie chart's configuration. Mutually exclusive with
2257 /// [`Self::basic_chart`].
2258 #[serde(default, skip_serializing_if = "Option::is_none", rename = "pieChart")]
2259 pub pie_chart: Option<PieChartSpec>,
2260 /// Every other `ChartSpec` field this crate doesn't model — including
2261 /// the other 13 chart-type variants — preserved verbatim across a
2262 /// read-merge-write cycle. See the module-level note above.
2263 #[serde(flatten)]
2264 pub extra: BTreeMap<String, serde_json::Value>,
2265}
2266
2267/// `BasicChartSpec` — the COLUMN/BAR/LINE/AREA/SCATTER subset this crate
2268/// supports (issue #1797's chosen v1 cut).
2269///
2270/// `COMBO` and `STEPPED_AREA` are also valid wire values for `chartType`,
2271/// and reading one back is tolerated (it lands here, not in `extra`, since
2272/// `chartType` is a named field) — but
2273/// `embedded_object.rs::merge_chart_spec` refuses to *update* one, since
2274/// `COMBO` needs a per-series `type` this crate doesn't model.
2275#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2276pub struct BasicChartSpec {
2277 /// `"COLUMN"`, `"BAR"`, `"LINE"`, `"AREA"`, `"SCATTER"` (this crate's
2278 /// supported set), or another wire value read back from an existing
2279 /// chart this crate didn't create. Defaulted defensively — see the
2280 /// module doc on proto3 zero-value omission (issue #1929).
2281 #[serde(default, rename = "chartType")]
2282 pub chart_type: String,
2283 /// Where the legend is drawn, e.g. `"BOTTOM_LEGEND"`, `"NONE"`.
2284 #[serde(
2285 default,
2286 skip_serializing_if = "Option::is_none",
2287 rename = "legendPosition"
2288 )]
2289 pub legend_position: Option<String>,
2290 /// `"NOT_STACKED"`, `"STACKED"`, or `"PERCENT_STACKED"`.
2291 #[serde(
2292 default,
2293 skip_serializing_if = "Option::is_none",
2294 rename = "stackedType"
2295 )]
2296 pub stacked_type: Option<String>,
2297 /// How many leading rows/columns of the source range are headers rather
2298 /// than data.
2299 #[serde(
2300 default,
2301 skip_serializing_if = "Option::is_none",
2302 rename = "headerCount"
2303 )]
2304 pub header_count: Option<i64>,
2305 /// The chart's axes.
2306 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2307 pub axis: Vec<BasicChartAxis>,
2308 /// The domain (typically the x-axis / category column).
2309 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2310 pub domains: Vec<BasicChartDomain>,
2311 /// The data series.
2312 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2313 pub series: Vec<BasicChartSeries>,
2314 /// Every other `BasicChartSpec` field this crate doesn't model
2315 /// (`threeDimensional`, `interpolateNulls`, `lineSmoothing`, …).
2316 #[serde(flatten)]
2317 pub extra: BTreeMap<String, serde_json::Value>,
2318}
2319
2320/// One axis of a [`BasicChartSpec`] — `BasicChartAxis`.
2321#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2322pub struct BasicChartAxis {
2323 /// Which physical axis this is — `"BOTTOM_AXIS"`, `"LEFT_AXIS"`, or
2324 /// `"RIGHT_AXIS"`. `--horizontal-axis-title`/`--vertical-axis-title`
2325 /// map to `BOTTOM_AXIS`/`LEFT_AXIS` literally — the physical axis, not
2326 /// the domain/series role, since a BAR chart's domain axis is
2327 /// `LEFT_AXIS`. Defaulted defensively — see the module doc on proto3
2328 /// zero-value omission (issue #1929).
2329 #[serde(default)]
2330 pub position: String,
2331 /// The axis title.
2332 #[serde(default, skip_serializing_if = "Option::is_none")]
2333 pub title: Option<String>,
2334 /// Every other `BasicChartAxis` field this crate doesn't model
2335 /// (`format`, `viewWindowOptions`, …).
2336 #[serde(flatten)]
2337 pub extra: BTreeMap<String, serde_json::Value>,
2338}
2339
2340/// One domain of a [`BasicChartSpec`] — `BasicChartDomain`.
2341#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2342pub struct BasicChartDomain {
2343 /// The domain's source data.
2344 pub domain: ChartData,
2345 /// Every other `BasicChartDomain` field this crate doesn't model
2346 /// (`reversed`).
2347 #[serde(flatten)]
2348 pub extra: BTreeMap<String, serde_json::Value>,
2349}
2350
2351/// One series of a [`BasicChartSpec`] — `BasicChartSeries`.
2352#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2353pub struct BasicChartSeries {
2354 /// The series' source data.
2355 pub series: ChartData,
2356 /// Which axis (by position) this series plots against — for a chart
2357 /// with more than one axis on the same side.
2358 #[serde(
2359 default,
2360 skip_serializing_if = "Option::is_none",
2361 rename = "targetAxis"
2362 )]
2363 pub target_axis: Option<String>,
2364 /// Every other `BasicChartSeries` field this crate doesn't model
2365 /// (`type` — COMBO-only, `color`, `lineStyle`, `styleOverrides`, …).
2366 #[serde(flatten)]
2367 pub extra: BTreeMap<String, serde_json::Value>,
2368}
2369
2370/// A pie chart's configuration — `PieChartSpec`.
2371#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2372pub struct PieChartSpec {
2373 /// The category labels (the pie's slices).
2374 pub domain: ChartData,
2375 /// The values (the slices' sizes).
2376 pub series: ChartData,
2377 /// Where the legend is drawn.
2378 #[serde(
2379 default,
2380 skip_serializing_if = "Option::is_none",
2381 rename = "legendPosition"
2382 )]
2383 pub legend_position: Option<String>,
2384 /// `0.0`-`1.0`: the radius of the center hole, as a fraction of the
2385 /// pie's radius. `0.0` (or absent) is a solid pie; anything above `0`
2386 /// is a donut.
2387 #[serde(default, skip_serializing_if = "Option::is_none", rename = "pieHole")]
2388 pub pie_hole: Option<f64>,
2389 /// Every other `PieChartSpec` field this crate doesn't model
2390 /// (`threeDimensional`).
2391 #[serde(flatten)]
2392 pub extra: BTreeMap<String, serde_json::Value>,
2393}
2394
2395/// One chart series or domain's source data — `ChartData`.
2396#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2397pub struct ChartData {
2398 /// The cell range(s) this series/domain reads from.
2399 #[serde(rename = "sourceRange")]
2400 pub source_range: ChartSourceRange,
2401 /// Every other `ChartData` field this crate doesn't model
2402 /// (`aggregateType`, `groupRule`, `columnReference`).
2403 #[serde(flatten)]
2404 pub extra: BTreeMap<String, serde_json::Value>,
2405}
2406
2407/// The cell range(s) backing one [`ChartData`] — `ChartSourceRange`.
2408///
2409/// A `Vec` on the wire (Sheets allows discontiguous sources for one
2410/// series), though this crate's builders only ever populate one.
2411#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2412pub struct ChartSourceRange {
2413 /// The range(s).
2414 pub sources: Vec<GridRange>,
2415}
2416
2417/// Where an [`EmbeddedChart`] or [`Slicer`] is anchored —
2418/// `EmbeddedObjectPosition`.
2419///
2420/// A three-way union on the wire: exactly one of `overlay_position`/
2421/// `new_sheet` is set on a chart this crate builds (a slicer can only ever
2422/// be `overlay_position` — the API has no `newSheet`/own-`sheetId` slicer
2423/// placement).
2424#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
2425pub struct EmbeddedObjectPosition {
2426 /// The chart occupies its own sheet, whose id this names. Read-back
2427 /// only — this crate never sets it directly; use `new_sheet` to
2428 /// request one.
2429 #[serde(default, skip_serializing_if = "Option::is_none", rename = "sheetId")]
2430 pub sheet_id: Option<i64>,
2431 /// The object floats over an existing sheet, anchored to a cell.
2432 #[serde(
2433 default,
2434 skip_serializing_if = "Option::is_none",
2435 rename = "overlayPosition"
2436 )]
2437 pub overlay_position: Option<OverlayPosition>,
2438 /// Request a brand-new sheet to hold this chart alone. Write-only
2439 /// (`--new-sheet`); the server never echoes `true` back — a
2440 /// server-created chart sheet reads back with `sheet_id` set instead.
2441 #[serde(default, skip_serializing_if = "Option::is_none", rename = "newSheet")]
2442 pub new_sheet: Option<bool>,
2443}
2444
2445/// An object floating over a sheet, anchored to a cell — `OverlayPosition`.
2446#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
2447pub struct OverlayPosition {
2448 /// The cell the object's top-left corner is anchored to.
2449 #[serde(rename = "anchorCell")]
2450 pub anchor_cell: GridCoordinate,
2451 /// Additional horizontal offset from the anchor cell, in pixels.
2452 #[serde(
2453 default,
2454 skip_serializing_if = "Option::is_none",
2455 rename = "offsetXPixels"
2456 )]
2457 pub offset_x_pixels: Option<i64>,
2458 /// Additional vertical offset from the anchor cell, in pixels.
2459 #[serde(
2460 default,
2461 skip_serializing_if = "Option::is_none",
2462 rename = "offsetYPixels"
2463 )]
2464 pub offset_y_pixels: Option<i64>,
2465 /// The object's width in pixels. Absent means the API's own default.
2466 #[serde(
2467 default,
2468 skip_serializing_if = "Option::is_none",
2469 rename = "widthPixels"
2470 )]
2471 pub width_pixels: Option<i64>,
2472 /// The object's height in pixels. Absent means the API's own default.
2473 #[serde(
2474 default,
2475 skip_serializing_if = "Option::is_none",
2476 rename = "heightPixels"
2477 )]
2478 pub height_pixels: Option<i64>,
2479}
2480
2481/// A single grid cell, addressed by sheet and 0-based row/column —
2482/// `GridCoordinate`. Distinct from [`GridRange`] (a rectangular span): an
2483/// anchor is one cell, never a range.
2484#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
2485pub struct GridCoordinate {
2486 /// The sheet this coordinate is on. Proto3 omits this on the wire when
2487 /// it's `0` (the first sheet) — see the module doc.
2488 #[serde(default, rename = "sheetId")]
2489 pub sheet_id: i64,
2490 /// 0-based row. Omitted on the wire when `0` (row 1) — a chart or
2491 /// slicer anchored at row 1 hits this.
2492 #[serde(default, rename = "rowIndex")]
2493 pub row_index: i64,
2494 /// 0-based column. Omitted on the wire when `0` (column A).
2495 #[serde(default, rename = "columnIndex")]
2496 pub column_index: i64,
2497}
2498
2499/// A slicer embedded on a sheet — `Slicer`.
2500///
2501/// Filters an existing range or pivot table interactively, the same
2502/// `FilterCriteria` vocabulary `filter.rs` curates (`hiddenValues` only —
2503/// see that module's doc comment for the condition-based cut this crate
2504/// makes uniformly).
2505#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2506pub struct Slicer {
2507 /// The server-assigned stable id. Absent on an `add-slicer` request
2508 /// this crate is building; always present on one read back or on an
2509 /// `update-slicer`/`delete-slicer` request.
2510 #[serde(default, skip_serializing_if = "Option::is_none", rename = "slicerId")]
2511 pub slicer_id: Option<i64>,
2512 /// The slicer's filtering configuration.
2513 #[serde(default, skip_serializing_if = "Option::is_none")]
2514 pub spec: Option<SlicerSpec>,
2515 /// Where the slicer is anchored. Always `overlay_position` — a slicer
2516 /// cannot occupy its own sheet.
2517 #[serde(default, skip_serializing_if = "Option::is_none")]
2518 pub position: Option<EmbeddedObjectPosition>,
2519}
2520
2521/// A slicer's filtering configuration — `SlicerSpec`.
2522#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2523pub struct SlicerSpec {
2524 /// The range the slicer filters.
2525 #[serde(default, skip_serializing_if = "Option::is_none", rename = "dataRange")]
2526 pub data_range: Option<GridRange>,
2527 /// Which values to hide, within `column_index`.
2528 #[serde(
2529 default,
2530 skip_serializing_if = "Option::is_none",
2531 rename = "filterCriteria"
2532 )]
2533 pub filter_criteria: Option<FilterCriteria>,
2534 /// The column the criteria apply to, as an absolute sheet column index
2535 /// (0 = column A) — confirmed live against #1945; see
2536 /// `embedded_object.rs`'s `--column` doc comment and
2537 /// `check_slicer_column_in_range`, which locally validates this against
2538 /// `data_range` before either `--dry-run` or a real run reaches the API.
2539 #[serde(
2540 default,
2541 skip_serializing_if = "Option::is_none",
2542 rename = "columnIndex"
2543 )]
2544 pub column_index: Option<i64>,
2545 /// Whether this slicer also filters pivot tables built from
2546 /// `data_range`.
2547 #[serde(
2548 default,
2549 skip_serializing_if = "Option::is_none",
2550 rename = "applyToPivotTables"
2551 )]
2552 pub apply_to_pivot_tables: Option<bool>,
2553 /// A human-readable name for the slicer.
2554 #[serde(default, skip_serializing_if = "Option::is_none")]
2555 pub title: Option<String>,
2556 /// Every other `SlicerSpec` field this crate doesn't model
2557 /// (`textFormat`, `backgroundColor`, `horizontalAlignment`, …).
2558 #[serde(flatten)]
2559 pub extra: BTreeMap<String, serde_json::Value>,
2560}
2561
2562/// `AddChartRequest`.
2563#[derive(Debug, Clone, Default, Serialize, PartialEq)]
2564pub struct AddChartRequest {
2565 /// The chart to create. `chart_id` is left unset — the server assigns
2566 /// it, only knowable from the reply.
2567 pub chart: EmbeddedChart,
2568}
2569
2570/// `UpdateChartSpecRequest`.
2571///
2572/// **No `fields` mask** — unlike every other update request in this file,
2573/// Sheets' `updateChartSpec` replaces the chart's entire `ChartSpec`
2574/// wholesale; there is no way to name "just the title". `embedded_object.rs`
2575/// fetches the existing spec, merges the caller's flags onto it (preserving
2576/// every unmodelled field via `extra`), and sends the full result here —
2577/// the same reason `update-filter-view` computes a full `sortSpecs`/
2578/// `criteria` client-side, except here it isn't optional: there is no
2579/// narrower request to fall back to.
2580#[derive(Debug, Clone, Serialize, PartialEq)]
2581pub struct UpdateChartSpecRequest {
2582 /// Which chart to update.
2583 #[serde(rename = "chartId")]
2584 pub chart_id: i64,
2585 /// The chart's new, complete spec.
2586 pub spec: ChartSpec,
2587}
2588
2589/// `AddSlicerRequest`.
2590#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
2591pub struct AddSlicerRequest {
2592 /// The slicer to create. `slicer_id` is left unset — the server assigns
2593 /// it, only knowable from the reply.
2594 pub slicer: Slicer,
2595}
2596
2597/// `UpdateSlicerSpecRequest`.
2598///
2599/// Unlike [`UpdateChartSpecRequest`], this request **does** carry a field
2600/// mask — `updateSlicerSpec` supports partial updates, so
2601/// `embedded_object.rs` only ever names the fields the caller actually set.
2602#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2603pub struct UpdateSlicerSpecRequest {
2604 /// Which slicer to update.
2605 #[serde(rename = "slicerId")]
2606 pub slicer_id: i64,
2607 /// The fields to write. `data_range`/`title`/`apply_to_pivot_tables`
2608 /// are independently settable; `filter_criteria` and `column_index`
2609 /// travel together (a criteria value is meaningless without knowing
2610 /// which column it filters).
2611 pub spec: SlicerSpec,
2612 /// The field mask limiting what this request may change.
2613 pub fields: String,
2614}
2615
2616/// `DeleteEmbeddedObjectRequest` — removes a chart or a slicer; the API
2617/// gives no way to name which kind `object_id` refers to, and none is
2618/// needed (issue #1797).
2619#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
2620pub struct DeleteEmbeddedObjectRequest {
2621 /// Which chart or slicer to remove.
2622 #[serde(rename = "objectId")]
2623 pub object_id: i64,
2624}
2625
2626/// `UpdateEmbeddedObjectPositionRequest` — moves and/or resizes an existing
2627/// chart or slicer (`move-chart`/`move-slicer`, issue #1837).
2628///
2629/// **The `fields` mask is rooted at `newPosition.overlayPosition`, not at
2630/// `newPosition`** — the API's own rule ("the root `newPosition.overlayPosition`
2631/// is implied and should not be specified"), and the one place this request
2632/// differs from every other masked request in this file, where the mask is
2633/// rooted at the request's own payload field (cf. [`UpdateSlicerSpecRequest`],
2634/// whose mask is rooted at `spec`). So a move sends `anchorCell`, never
2635/// `overlayPosition.anchorCell`. The mask is used **only** when
2636/// `new_position.overlay_position` is set — a `newSheet` move sends no
2637/// `fields` at all (`skip_serializing_if = "String::is_empty"`), since there
2638/// is nothing under `overlayPosition` to name.
2639#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2640pub struct UpdateEmbeddedObjectPositionRequest {
2641 /// Which chart or slicer to move.
2642 #[serde(rename = "objectId")]
2643 pub object_id: i64,
2644 /// The position to write.
2645 #[serde(rename = "newPosition")]
2646 pub new_position: EmbeddedObjectPosition,
2647 /// The field mask limiting what this request may change, relative to
2648 /// `newPosition.overlayPosition`. Empty (and omitted on the wire) for a
2649 /// `--new-sheet` move.
2650 #[serde(skip_serializing_if = "String::is_empty")]
2651 pub fields: String,
2652}
2653
2654/// `UpdateEmbeddedObjectBorderRequest`.
2655///
2656/// Sets or clears an existing chart's border colour (`update-chart-border`,
2657/// issue #1837). Charts only; a [`Slicer`] carries no `border` field, so
2658/// there is no `update-slicer-border` counterpart.
2659///
2660/// Confirmed live (issue #1929): clearing and setting an explicit black
2661/// (`0,0,0`) both read back as the same wire shape — see
2662/// `embedded_object.rs`'s module doc and `describe_border_color`.
2663#[derive(Debug, Clone, Serialize, PartialEq)]
2664pub struct UpdateEmbeddedObjectBorderRequest {
2665 /// Which chart to update.
2666 #[serde(rename = "objectId")]
2667 pub object_id: i64,
2668 /// The border to write — `EmbeddedObjectBorder::default()` (no
2669 /// `colorStyle`) clears it.
2670 pub border: EmbeddedObjectBorder,
2671 /// The field mask limiting what this request may change. Always
2672 /// `"colorStyle"` — the only field [`EmbeddedObjectBorder`] models —
2673 /// whether setting a colour or clearing one.
2674 pub fields: String,
2675}
2676
2677/// `updateCells` — issue #1798's `add-pivot-table`/`delete-pivot-table`,
2678/// the crate's only user of this request (see
2679/// [`BatchUpdateRequestItem::UpdateCells`]'s doc comment).
2680///
2681/// Always exactly one row of one cell: a pivot table has exactly one
2682/// anchor, so `rows` is a single-element `Vec` rather than a genuine grid
2683/// — kept as `Vec` because that is the wire shape, not because this crate
2684/// ever sends more than one.
2685#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
2686pub struct UpdateCellsRequest {
2687 /// The anchor cell.
2688 pub start: GridCoordinate,
2689 /// Always one [`PivotRowData`] of one [`PivotCellData`].
2690 pub rows: Vec<PivotRowData>,
2691 /// Always the literal `"pivotTable"` — the field mask that makes this
2692 /// request touch nothing but the anchor's `pivotTable` property,
2693 /// preserving whatever value or formatting the cell already had.
2694 pub fields: String,
2695}
2696
2697/// One row of [`UpdateCellsRequest::rows`].
2698#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
2699pub struct PivotRowData {
2700 /// The row's cells.
2701 pub values: Vec<PivotCellData>,
2702}
2703
2704/// The `cell` payload of an `updateCells` request — deliberately **has no
2705/// `userEnteredValue` field**.
2706///
2707/// Same guarantee [`RepeatCellData`]'s doc comment describes: there is
2708/// nowhere on this type to put a literal value, so `add-pivot-table`
2709/// cannot be used to write one regardless of what a caller asks for.
2710#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
2711pub struct PivotCellData {
2712 /// The pivot table to write, or `None` to clear an existing one
2713 /// (`delete-pivot-table`) — serializes as `{}` when absent, which is
2714 /// what `fields: "pivotTable"` interprets as "clear this property".
2715 #[serde(
2716 default,
2717 skip_serializing_if = "Option::is_none",
2718 rename = "pivotTable"
2719 )]
2720 pub pivot_table: Option<PivotTable>,
2721}
2722
2723/// A pivot table definition — `PivotTable`.
2724///
2725/// Also `Deserialize`: unlike most other request-only pieces in this
2726/// file, a pivot table is read back too, via
2727/// [`CellSnapshot::pivot_table`], so `list-pivot-tables` and
2728/// `delete-pivot-table`'s "currently:" preview can round-trip one.
2729///
2730/// **A curated surface, not full API coverage** — mirroring
2731/// `conditional_format.rs`'s own stance. Not modelled: the deprecated
2732/// `criteria` field (`filterSpecs` replaces it), `PivotGroup.groupRule`
2733/// (date/number bucketing), `PivotGroup.valueBucket`/`valueMetadata`
2734/// (sort-by-value-column and collapsed-group state), data-source pivots
2735/// (`dataSourceId`/`dataExecutionStatus`), and
2736/// `PivotValue.calculatedDisplayType`. `docs/drive.md` names each gap.
2737#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2738#[serde(rename_all = "camelCase")]
2739pub struct PivotTable {
2740 /// The range this pivot table is built from.
2741 pub source: GridRange,
2742 /// Row grouping(s), outermost first.
2743 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2744 pub rows: Vec<PivotGroup>,
2745 /// Column grouping(s), outermost first.
2746 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2747 pub columns: Vec<PivotGroup>,
2748 /// Aggregated value column(s).
2749 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2750 pub values: Vec<PivotValue>,
2751 /// Source-row filters, by column offset.
2752 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "filterSpecs")]
2753 pub filter_specs: Vec<PivotFilterSpec>,
2754 /// `"HORIZONTAL"` (values as columns, the API default) or
2755 /// `"VERTICAL"` (values as rows). Absent means the API default.
2756 #[serde(
2757 default,
2758 skip_serializing_if = "Option::is_none",
2759 rename = "valueLayout"
2760 )]
2761 pub value_layout: Option<String>,
2762}
2763
2764/// One row or column grouping of a [`PivotTable`] — `PivotGroup`.
2765#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2766#[serde(rename_all = "camelCase")]
2767pub struct PivotGroup {
2768 /// Which source column this groups by, 0-based and relative to
2769 /// [`PivotTable::source`]'s first column — not an absolute sheet
2770 /// column index. Proto3 omits this on the wire when it's `0`
2771 /// (grouping by the source's first column) — see the module doc.
2772 #[serde(default, rename = "sourceColumnOffset")]
2773 pub source_column_offset: i64,
2774 /// Whether to show a totals row/column for this grouping. Always sent
2775 /// explicitly (never omitted) since the API's own default (`true`)
2776 /// would otherwise silently differ from a caller's expectation.
2777 /// `#[serde(default)]` only affects *deserialisation*: it's needed
2778 /// because proto3 omits `false` on the wire (`--no-totals`), and
2779 /// doesn't change what this crate sends.
2780 #[serde(default, rename = "showTotals")]
2781 pub show_totals: bool,
2782 /// `"ASCENDING"` or `"DESCENDING"`. Optional only so a read-back of
2783 /// an existing pivot table tolerates its absence: the API rejects a
2784 /// write without it (`No sort order specified.`, #1930), so
2785 /// `add-pivot-table` always sends one, defaulting a bare `COLUMN` to
2786 /// `"ASCENDING"`.
2787 #[serde(default, skip_serializing_if = "Option::is_none", rename = "sortOrder")]
2788 pub sort_order: Option<String>,
2789}
2790
2791/// One aggregated value column of a [`PivotTable`] — `PivotValue`.
2792#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2793#[serde(rename_all = "camelCase")]
2794pub struct PivotValue {
2795 /// Which source column this aggregates, 0-based and relative to
2796 /// [`PivotTable::source`]'s first column. Proto3 omits this on the wire
2797 /// when it's `0` (aggregating the source's first column) — see the
2798 /// module doc.
2799 #[serde(default, rename = "sourceColumnOffset")]
2800 pub source_column_offset: i64,
2801 /// One of Sheets' `SUM`/`COUNTA`/`COUNT`/`COUNTUNIQUE`/`AVERAGE`/
2802 /// `MAX`/`MIN`/`MEDIAN`/`PRODUCT`/`STDEV`/`STDEVP`/`VAR`/`VARP`
2803 /// (`CUSTOM`, a value driven by a formula rather than a source column,
2804 /// is a documented cut). A plain string, the same tolerate-unmodelled
2805 /// stance as [`InterpolationPoint::point_type`]. Defaulted defensively
2806 /// like that field — see the module doc on proto3 zero-value omission
2807 /// (issue #1929).
2808 #[serde(default, rename = "summarizeFunction")]
2809 pub summarize_function: String,
2810 /// An optional display name overriding the default
2811 /// (`"<function> of <column>"`).
2812 #[serde(default, skip_serializing_if = "Option::is_none")]
2813 pub name: Option<String>,
2814}
2815
2816/// A source-row filter on one column of a [`PivotTable`] —
2817/// `PivotFilterSpec`.
2818///
2819/// Only the `filterCriteria` form is modelled (a fixed allow-list of
2820/// values); the newer `dataSourceColumnReference`, meaningful only for
2821/// data-source pivots, is a documented cut alongside them.
2822#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2823#[serde(rename_all = "camelCase")]
2824pub struct PivotFilterSpec {
2825 /// Which source column this filters, 0-based and relative to
2826 /// [`PivotTable::source`]'s first column. Proto3 omits this on the
2827 /// wire when it's `0` (filtering the source's first column) — see the
2828 /// module doc.
2829 #[serde(default, rename = "columnOffsetIndex")]
2830 pub column_offset_index: i64,
2831 /// The filter itself.
2832 #[serde(rename = "filterCriteria")]
2833 pub filter_criteria: PivotFilterCriteria,
2834}
2835
2836/// [`PivotFilterSpec`]'s filter.
2837///
2838/// Only `visibleValues` (a fixed allow-list of raw string values) is
2839/// modelled; the API's condition-based form (`BooleanCondition`,
2840/// mirroring data validation) is a documented cut.
2841#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
2842#[serde(rename_all = "camelCase")]
2843pub struct PivotFilterCriteria {
2844 /// The raw source values that pass this filter.
2845 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2846 pub visible_values: Vec<String>,
2847}
2848
2849/// One row-major chunk of a sheet's cell data — `GridData`.
2850///
2851/// The API chunks rather than returning one flat grid because a `fields`
2852/// mask can request disjoint ranges in one call; this crate only ever
2853/// requests one contiguous range at a time, so `Sheet::data` is a
2854/// single-element `Vec` in practice, but the type stays a `Vec` because
2855/// that is the wire shape.
2856///
2857/// `PartialEq` only, **not** `Eq` (issue #1878) — see [`CellSnapshot`]'s doc
2858/// comment.
2859#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2860#[serde(rename_all = "camelCase")]
2861pub struct GridData {
2862 /// The zero-based row this chunk starts at.
2863 #[serde(default, skip_serializing_if = "Option::is_none")]
2864 pub start_row: Option<i64>,
2865 /// The zero-based column this chunk starts at.
2866 #[serde(default, skip_serializing_if = "Option::is_none")]
2867 pub start_column: Option<i64>,
2868 /// The rows themselves.
2869 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2870 pub row_data: Vec<RowData>,
2871}
2872
2873/// One row of [`GridData::row_data`].
2874///
2875/// `PartialEq` only, **not** `Eq` (issue #1878) — see [`CellSnapshot`]'s doc
2876/// comment.
2877#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2878pub struct RowData {
2879 /// The row's cells, in the `fields` mask the caller requested — every
2880 /// property not asked for is simply absent, even one that exists on
2881 /// the real cell.
2882 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2883 pub values: Vec<CellSnapshot>,
2884}
2885
2886/// Read-only mirror of [`ColorStyle`], tolerant of the API's alternative
2887/// `themeColor` arm (issue #1878).
2888///
2889/// [`ColorStyle`] itself requires `rgbColor`, because every write this crate
2890/// sends carries an explicit RGB color. But a cell's *existing*
2891/// `userEnteredFormat` can carry a theme color instead — set via the
2892/// Sheets UI's "Theme colors" picker — which comes back as
2893/// `{"themeColor": "TEXT"}` with **no** `rgbColor` key at all, and would
2894/// fail to deserialize into [`ColorStyle`]. Exactly one of the two fields is
2895/// ever present; `read-cell-format`'s rendering falls back to naming the
2896/// theme color (`theme:TEXT`) when `rgb_color` is absent, rather than
2897/// defaulting it to a misleading black.
2898#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2899pub struct ColorStyleSnapshot {
2900 /// The explicit RGB color, when this is the `rgbColor` arm.
2901 #[serde(default, rename = "rgbColor", skip_serializing_if = "Option::is_none")]
2902 pub rgb_color: Option<Color>,
2903 /// The theme color's name (e.g. `"TEXT"`, `"ACCENT1"`), when this is the
2904 /// `themeColor` arm.
2905 #[serde(
2906 default,
2907 rename = "themeColor",
2908 skip_serializing_if = "Option::is_none"
2909 )]
2910 pub theme_color: Option<String>,
2911}
2912
2913/// Read-only mirror of [`TextFormat`] (issue #1878's `read-cell-format`).
2914///
2915/// Covers exactly the sub-fields its `fields` mask requests: bold, italic,
2916/// strikethrough, underline and the foreground color. Kept separate from
2917/// [`TextFormat`] only because its color field is [`ColorStyleSnapshot`],
2918/// not [`ColorStyle`] — see that type's doc comment.
2919#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2920pub struct TextFormatSnapshot {
2921 /// Bold.
2922 #[serde(default, skip_serializing_if = "Option::is_none")]
2923 pub bold: Option<bool>,
2924 /// Italic.
2925 #[serde(default, skip_serializing_if = "Option::is_none")]
2926 pub italic: Option<bool>,
2927 /// Strikethrough.
2928 #[serde(default, skip_serializing_if = "Option::is_none")]
2929 pub strikethrough: Option<bool>,
2930 /// Underline.
2931 #[serde(default, skip_serializing_if = "Option::is_none")]
2932 pub underline: Option<bool>,
2933 /// Text color.
2934 #[serde(
2935 default,
2936 skip_serializing_if = "Option::is_none",
2937 rename = "foregroundColorStyle"
2938 )]
2939 pub foreground_color_style: Option<ColorStyleSnapshot>,
2940}
2941
2942/// Read-only mirror of [`CellFormat`] (issue #1878's `read-cell-format`).
2943///
2944/// Covers exactly the sub-fields its `fields` mask requests: background
2945/// color, text format, number format and horizontal alignment — the
2946/// properties ADR-0083 §5's "does this verb move formatting?" question
2947/// turns on. Kept separate from [`CellFormat`] for the same reason
2948/// [`CellSnapshot`]'s doc comment gives for staying separate from
2949/// [`PivotCellData`]: a read-only type must never become a path for
2950/// writing a format back.
2951#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2952pub struct CellFormatSnapshot {
2953 /// Cell background color.
2954 #[serde(
2955 default,
2956 skip_serializing_if = "Option::is_none",
2957 rename = "backgroundColorStyle"
2958 )]
2959 pub background_color_style: Option<ColorStyleSnapshot>,
2960 /// Text formatting (bold, italic, color, …).
2961 #[serde(
2962 default,
2963 skip_serializing_if = "Option::is_none",
2964 rename = "textFormat"
2965 )]
2966 pub text_format: Option<TextFormatSnapshot>,
2967 /// Display number format.
2968 #[serde(
2969 default,
2970 skip_serializing_if = "Option::is_none",
2971 rename = "numberFormat"
2972 )]
2973 pub number_format: Option<NumberFormat>,
2974 /// `"LEFT"` / `"CENTER"` / `"RIGHT"`.
2975 #[serde(
2976 default,
2977 skip_serializing_if = "Option::is_none",
2978 rename = "horizontalAlignment"
2979 )]
2980 pub horizontal_alignment: Option<String>,
2981 /// The four outer edge borders (issue #1878). A border is still part of
2982 /// `userEnteredFormat`, so omitting it here would let a verb that moves
2983 /// only borders show as "no formatting changed".
2984 #[serde(default, skip_serializing_if = "Option::is_none")]
2985 pub borders: Option<BordersSnapshot>,
2986}
2987
2988/// Read-only mirror of the write-side [`Border`] (issue #1878).
2989///
2990/// Tolerant of a `colorStyle` carrying a theme color — see
2991/// [`ColorStyleSnapshot`]'s doc comment. [`Border`] itself has no
2992/// `Deserialize` impl (it is write-only), so this is a genuinely separate
2993/// type, not just a field-level patch.
2994#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2995pub struct BorderSnapshot {
2996 /// `"SOLID"` / `"SOLID_MEDIUM"` / `"SOLID_THICK"` / `"DASHED"` /
2997 /// `"DOTTED"` / `"DOUBLE"` / `"NONE"`.
2998 #[serde(default)]
2999 pub style: String,
3000 /// The line color.
3001 #[serde(
3002 default,
3003 skip_serializing_if = "Option::is_none",
3004 rename = "colorStyle"
3005 )]
3006 pub color_style: Option<ColorStyleSnapshot>,
3007}
3008
3009/// The four outer edges of `CellFormatSnapshot.borders` (issue #1878).
3010///
3011/// Mirrors [`UpdateBordersRequest`]'s shape, but read-only. Each edge is
3012/// independently optional: an unset edge simply wasn't requested-and-present
3013/// on this cell, the same "absent means not asked for or not set"
3014/// convention every other `*Snapshot` field follows.
3015#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3016pub struct BordersSnapshot {
3017 /// Top edge.
3018 #[serde(default, skip_serializing_if = "Option::is_none")]
3019 pub top: Option<BorderSnapshot>,
3020 /// Bottom edge.
3021 #[serde(default, skip_serializing_if = "Option::is_none")]
3022 pub bottom: Option<BorderSnapshot>,
3023 /// Left edge.
3024 #[serde(default, skip_serializing_if = "Option::is_none")]
3025 pub left: Option<BorderSnapshot>,
3026 /// Right edge.
3027 #[serde(default, skip_serializing_if = "Option::is_none")]
3028 pub right: Option<BorderSnapshot>,
3029}
3030
3031impl BordersSnapshot {
3032 /// Whether every edge is absent — used to skip an empty `borders`
3033 /// object from a table-line rendering the same way a cell carrying no
3034 /// format at all is skipped.
3035 #[must_use]
3036 pub fn is_empty(&self) -> bool {
3037 self.top.is_none() && self.bottom.is_none() && self.left.is_none() && self.right.is_none()
3038 }
3039}
3040
3041/// One cell, read back only for the properties a caller's `fields` mask
3042/// requested — `CellSnapshot`.
3043///
3044/// Distinct from [`PivotCellData`] (the *write*-side, value-incapable
3045/// type) precisely so this read-only type can carry
3046/// [`Self::formatted_value`] without ever creating a path for a literal
3047/// value to be written back through it.
3048///
3049/// `PartialEq` only, **not** `Eq` (issue #1878): [`Self::user_entered_format`]
3050/// carries [`Color`]'s `f32` channels transitively, which has no meaningful
3051/// `Eq` — see that type's doc comment. [`RowData`] and [`GridData`] drop
3052/// `Eq` for the same reason.
3053#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3054#[serde(rename_all = "camelCase")]
3055pub struct CellSnapshot {
3056 /// The pivot table anchored at this cell, if any.
3057 #[serde(default, skip_serializing_if = "Option::is_none")]
3058 pub pivot_table: Option<PivotTable>,
3059 /// The cell's value as displayed in the UI — used only to describe an
3060 /// occupied anchor in a refusal/dry-run message, never parsed.
3061 #[serde(default, skip_serializing_if = "Option::is_none")]
3062 pub formatted_value: Option<String>,
3063 /// The cell's user-entered format — background, text formatting, number
3064 /// format, horizontal alignment (issue #1878's `read-cell-format`).
3065 /// Deliberately **`userEnteredFormat`, not `effectiveFormat`**: what a
3066 /// sort or fill physically moves is the user-entered format, while
3067 /// `effectiveFormat` folds in conditional formatting, which follows the
3068 /// *range* rather than the cell and would give false positives when
3069 /// checking whether a verb moved formatting.
3070 #[serde(
3071 default,
3072 skip_serializing_if = "Option::is_none",
3073 rename = "userEnteredFormat"
3074 )]
3075 pub user_entered_format: Option<CellFormatSnapshot>,
3076 /// A note attached to the cell (issue #1878).
3077 #[serde(default, skip_serializing_if = "Option::is_none")]
3078 pub note: Option<String>,
3079 /// A data validation rule on the cell (issue #1878).
3080 #[serde(
3081 default,
3082 skip_serializing_if = "Option::is_none",
3083 rename = "dataValidation"
3084 )]
3085 pub data_validation: Option<DataValidationRule>,
3086}
3087
3088/// `InsertDimensionRequest`.
3089#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3090pub struct InsertDimensionRequest {
3091 /// Where to insert.
3092 pub range: DimensionRange,
3093 /// Whether the inserted rows/columns inherit formatting from the ones
3094 /// before them rather than after. Always `false` here: `true` is
3095 /// rejected outright by the API when `start_index` is 0, so a fixed
3096 /// `false` is the only value that works for every legal `--at`.
3097 #[serde(rename = "inheritFromBefore")]
3098 pub inherit_from_before: bool,
3099}
3100
3101/// `DeleteSheetRequest` — names the sheet to remove entirely.
3102#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3103pub struct DeleteSheetRequest {
3104 /// The sheet to delete.
3105 #[serde(rename = "sheetId")]
3106 pub sheet_id: i64,
3107}
3108
3109/// `DeleteDimensionRequest` — deletes the rows or columns spanned by `range`.
3110///
3111/// Shifts the remainder to close the gap. Reuses [`DimensionRange`] as-is:
3112/// the request shape is identical to [`InsertDimensionRequest`]'s `range`,
3113/// just interpreted as removal instead of insertion.
3114#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3115pub struct DeleteDimensionRequest {
3116 /// The rows or columns to delete.
3117 pub range: DimensionRange,
3118}
3119
3120/// `MoveDimensionRequest` — moves the rows or columns spanned by `source` to
3121/// a new position within the same sheet (`move-rows`/`move-columns`, issue
3122/// #1834).
3123///
3124/// Nothing is discarded: the moved rows/columns take their values with them
3125/// and the rows/columns between the source and the destination shift to
3126/// close the gap. Reuses [`DimensionRange`] as-is, exactly as
3127/// [`DeleteDimensionRequest`] does.
3128#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3129pub struct MoveDimensionRequest {
3130 /// The rows or columns to move.
3131 pub source: DimensionRange,
3132 /// The zero-based index the source is moved *in front of*, in the
3133 /// sheet's coordinates **before** the source is removed from the grid —
3134 /// the API's own definition, so a downward move lands `source.len()`
3135 /// short of this number. The CLI's 1-based `--before` is converted in
3136 /// `structure.rs::move_destination_index`, never at a call site.
3137 #[serde(rename = "destinationIndex")]
3138 pub destination_index: i64,
3139}
3140
3141/// Which axis a range operation shifts cells along.
3142#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
3143pub enum ShiftDimension {
3144 /// Cells below a range shift up after deletion or down after insertion.
3145 #[serde(rename = "ROWS")]
3146 Rows,
3147 /// Cells to the right of a range shift left after deletion or right after
3148 /// insertion.
3149 #[serde(rename = "COLUMNS")]
3150 Columns,
3151}
3152
3153impl ShiftDimension {
3154 /// The wire spelling, also used in human-readable output.
3155 #[must_use]
3156 pub fn as_str(self) -> &'static str {
3157 match self {
3158 Self::Rows => "ROWS",
3159 Self::Columns => "COLUMNS",
3160 }
3161 }
3162}
3163
3164/// Same two axes as [`Dimension`] but for a range shift rather than a
3165/// `DimensionRange`; kept as a distinct type since the two operations are
3166/// conceptually different, but the axis itself converts freely so callers
3167/// that describe a shift direction (e.g. `structure.rs::shift_word`) can
3168/// share one implementation.
3169impl From<ShiftDimension> for Dimension {
3170 fn from(shift: ShiftDimension) -> Self {
3171 match shift {
3172 ShiftDimension::Rows => Self::Rows,
3173 ShiftDimension::Columns => Self::Columns,
3174 }
3175 }
3176}
3177
3178/// `InsertRangeRequest` — inserts empty cells into `range`, shifting existing
3179/// cells along `shift_dimension` to make room.
3180///
3181/// Cells shifted past the sheet's grid extent are dropped by the Sheets API.
3182#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3183pub struct InsertRangeRequest {
3184 /// The empty cells to insert.
3185 pub range: GridRange,
3186 /// Which way existing cells shift to make room.
3187 #[serde(rename = "shiftDimension")]
3188 pub shift_dimension: ShiftDimension,
3189}
3190
3191/// `DeleteRangeRequest` — deletes the cells in `range`, shifting the
3192/// remainder along `shift_dimension` to close the gap.
3193#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3194pub struct DeleteRangeRequest {
3195 /// The cells to delete.
3196 pub range: GridRange,
3197 /// Which way to shift the remaining cells afterward.
3198 #[serde(rename = "shiftDimension")]
3199 pub shift_dimension: ShiftDimension,
3200}
3201
3202/// A banded range — alternating row or column colors applied to a range
3203/// (issue #1832, [ADR-0082](../../../docs/adrs/adr-0082-banded-ranges.md)).
3204///
3205/// v1 sets at most one of `row_properties`/`column_properties` per
3206/// `add-banding`/`update-banding` call, selected by `--axis`; the API
3207/// itself allows both on the same range simultaneously (a checkerboard
3208/// effect), which this crate does not expose — a documented cut, not a
3209/// silent gap.
3210///
3211/// `PartialEq`-only, not `Eq`: embeds [`ColorStyle`], which wraps the
3212/// `f32`-based [`Color`] — see [`Color`]'s own doc comment.
3213#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3214pub struct BandedRange {
3215 /// The server-assigned stable id. Absent on an `add-banding` request
3216 /// this crate is building; always present on one read back or on an
3217 /// `update-banding`/`delete-banding` request.
3218 #[serde(
3219 default,
3220 skip_serializing_if = "Option::is_none",
3221 rename = "bandedRangeId"
3222 )]
3223 pub banded_range_id: Option<i64>,
3224 /// The banded range.
3225 #[serde(default, skip_serializing_if = "Option::is_none")]
3226 pub range: Option<GridRange>,
3227 /// Row-banding colors, when banding alternates by row.
3228 #[serde(
3229 default,
3230 skip_serializing_if = "Option::is_none",
3231 rename = "rowProperties"
3232 )]
3233 pub row_properties: Option<BandingProperties>,
3234 /// Column-banding colors, when banding alternates by column.
3235 #[serde(
3236 default,
3237 skip_serializing_if = "Option::is_none",
3238 rename = "columnProperties"
3239 )]
3240 pub column_properties: Option<BandingProperties>,
3241}
3242
3243/// The colors of one axis of a [`BandedRange`] — `BandingProperties`.
3244///
3245/// Only the modern `*ColorStyle` fields are modelled, matching
3246/// `format-cells`'s precedent verbatim: the API's plain `headerColor`/
3247/// `firstBandColor`/`secondBandColor`/`footerColor` fields are deprecated
3248/// in favor of their `*ColorStyle` counterparts, which take precedence when
3249/// both are set, so the plain fields are never worth sending. Within each
3250/// `ColorStyle`, only the `rgbColor` arm is modelled — see [`ColorStyle`]'s
3251/// own doc comment.
3252#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
3253pub struct BandingProperties {
3254 /// The header row/column's color, if distinct from the alternating
3255 /// bands.
3256 #[serde(
3257 default,
3258 skip_serializing_if = "Option::is_none",
3259 rename = "headerColorStyle"
3260 )]
3261 pub header_color_style: Option<ColorStyle>,
3262 /// The first band's color.
3263 #[serde(
3264 default,
3265 skip_serializing_if = "Option::is_none",
3266 rename = "firstBandColorStyle"
3267 )]
3268 pub first_band_color_style: Option<ColorStyle>,
3269 /// The second (alternating) band's color.
3270 #[serde(
3271 default,
3272 skip_serializing_if = "Option::is_none",
3273 rename = "secondBandColorStyle"
3274 )]
3275 pub second_band_color_style: Option<ColorStyle>,
3276 /// The footer row/column's color, if distinct from the alternating
3277 /// bands.
3278 #[serde(
3279 default,
3280 skip_serializing_if = "Option::is_none",
3281 rename = "footerColorStyle"
3282 )]
3283 pub footer_color_style: Option<ColorStyle>,
3284}
3285
3286/// `AddBandingRequest`.
3287#[derive(Debug, Clone, Serialize, PartialEq)]
3288pub struct AddBandingRequest {
3289 /// The banded range to create. `banded_range_id` is left unset — the
3290 /// server assigns it, only knowable from the reply.
3291 #[serde(rename = "bandedRange")]
3292 pub banded_range: BandedRange,
3293}
3294
3295/// `UpdateBandingRequest`.
3296///
3297/// Reuses [`BandedRange`] itself: every
3298/// field here (`range`/`rowProperties`/`columnProperties`) is
3299/// independently updatable, and `banded_range_id` must be set to select the
3300/// target.
3301#[derive(Debug, Clone, Serialize, PartialEq)]
3302pub struct UpdateBandingRequest {
3303 /// The properties to write. `banded_range_id` selects the target.
3304 #[serde(rename = "bandedRange")]
3305 pub banded_range: BandedRange,
3306 /// The field mask limiting what this request may change.
3307 pub fields: String,
3308}
3309
3310/// `DeleteBandingRequest`.
3311#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
3312pub struct DeleteBandingRequest {
3313 /// Which banded range to remove.
3314 #[serde(rename = "bandedRangeId")]
3315 pub banded_range_id: i64,
3316}
3317
3318/// `AddDimensionGroupRequest`.
3319#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3320pub struct AddDimensionGroupRequest {
3321 /// The span to create a group over. The server derives the new
3322 /// group's `depth` from this; see [`DimensionGroup`]'s doc comment.
3323 pub range: DimensionRange,
3324}
3325
3326/// `UpdateDimensionGroupRequest`.
3327///
3328/// `collapsed` is the only field this crate ever changes — `range` is the
3329/// group's identity and `depth` is server-derived, so both are always sent
3330/// unchanged from the group `update-dimension-group` resolved against,
3331/// with `fields` naming only `"collapsed"`.
3332#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3333pub struct UpdateDimensionGroupRequest {
3334 /// The group to update, identified by its (unchanged) `range` and
3335 /// `depth`, carrying the new `collapsed` value.
3336 #[serde(rename = "dimensionGroup")]
3337 pub dimension_group: DimensionGroup,
3338 /// The field mask limiting what this request may change. Always
3339 /// `"collapsed"` — the only field [`Self::dimension_group`] ever
3340 /// updates.
3341 pub fields: String,
3342}
3343
3344/// `DeleteDimensionGroupRequest`.
3345///
3346/// Addressed by `range` alone, like the API — `depth` plays no part in
3347/// which group is removed. See the `dimension_group.rs` module doc for why
3348/// this crate requires an **exact** range match rather than exposing the
3349/// API's partial-overlap decrement semantics.
3350#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3351pub struct DeleteDimensionGroupRequest {
3352 /// The span whose group should be removed.
3353 pub range: DimensionRange,
3354}
3355
3356/// `PasteType` — what a paste request carries, per the Sheets API's own
3357/// enum (issue #1839, [ADR-0083](../../../docs/adrs/adr-0083.md) §4).
3358///
3359/// **Curated: four of the API's seven variants.** `PASTE_NORMAL`,
3360/// `PASTE_VALUES`, `PASTE_FORMULA` and `PASTE_FORMAT` cover the common
3361/// cases (a plain copy or cut, a values-only or formula-only copy, and a
3362/// format-only copy); `PASTE_NO_BORDERS`, `PASTE_DATA_VALIDATION` and
3363/// `PASTE_CONDITIONAL_FORMATTING` are deferred — a documented cut, not a
3364/// silent gap (`docs/drive.md`'s cut-paste/copy-paste/paste-data section
3365/// names it). No default arm anywhere this is matched: a variant this enum
3366/// doesn't carry is unrepresentable, and the one place a `(writes_values,
3367/// writes_presentation)` pair is turned into a gate resolves an
3368/// unrecognised combination to *both* operations, so a type added here
3369/// without updating that table can only ever be stricter than intended,
3370/// never weaker.
3371#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
3372pub enum PasteType {
3373 /// Values, formulas, formats and merges — the API's own default and
3374 /// what "paste" means to a Sheets user. Resolves **both**
3375 /// `SheetsWrite` and `SheetsStructure` (ADR-0083 §4).
3376 #[serde(rename = "PASTE_NORMAL")]
3377 Normal,
3378 /// Cell content only, evaluated formulas rendered to their values.
3379 /// Resolves `SheetsWrite` alone.
3380 #[serde(rename = "PASTE_VALUES")]
3381 Values,
3382 /// Cell content only, formulas preserved. Resolves `SheetsWrite` alone.
3383 #[serde(rename = "PASTE_FORMULA")]
3384 Formula,
3385 /// The cell format only, excluding data validation. Resolves
3386 /// `SheetsStructure` alone.
3387 #[serde(rename = "PASTE_FORMAT")]
3388 Format,
3389}
3390
3391impl PasteType {
3392 /// The wire spelling, also used in human-readable output.
3393 #[must_use]
3394 pub fn as_str(self) -> &'static str {
3395 match self {
3396 Self::Normal => "PASTE_NORMAL",
3397 Self::Values => "PASTE_VALUES",
3398 Self::Formula => "PASTE_FORMULA",
3399 Self::Format => "PASTE_FORMAT",
3400 }
3401 }
3402
3403 /// Whether this paste type writes cell content — the half `SheetsWrite`
3404 /// covers.
3405 #[must_use]
3406 pub const fn writes_values(self) -> bool {
3407 matches!(self, Self::Normal | Self::Values | Self::Formula)
3408 }
3409
3410 /// Whether this paste type writes formats or merges — the half
3411 /// `SheetsStructure` covers.
3412 #[must_use]
3413 pub const fn writes_presentation(self) -> bool {
3414 matches!(self, Self::Normal | Self::Format)
3415 }
3416}
3417
3418/// `PasteOrientation` — `copy-paste`'s `--orientation` (issue #1839,
3419/// ADR-0083 §4). Changes no gate; `Transpose` swaps the source's
3420/// dimensions before the written extent is computed.
3421#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Default)]
3422pub enum PasteOrientation {
3423 /// Rows stay rows, columns stay columns — the API's own default.
3424 #[default]
3425 #[serde(rename = "NORMAL")]
3426 Normal,
3427 /// Rows and columns are swapped.
3428 #[serde(rename = "TRANSPOSE")]
3429 Transpose,
3430}
3431
3432impl PasteOrientation {
3433 /// The wire spelling, also used in human-readable output.
3434 #[must_use]
3435 pub fn as_str(self) -> &'static str {
3436 match self {
3437 Self::Normal => "NORMAL",
3438 Self::Transpose => "TRANSPOSE",
3439 }
3440 }
3441}
3442
3443/// `CutPasteRequest` — moves `source` to `destination` (a single cell; the
3444/// pasted block extends from there), clearing `source` entirely regardless
3445/// of `paste_type`.
3446#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3447pub struct CutPasteRequest {
3448 /// The range to move.
3449 pub source: GridRange,
3450 /// The top-left cell of the destination.
3451 pub destination: GridCoordinate,
3452 /// What to carry over — always both operations' worth of consent is
3453 /// required to send this request (ADR-0083 §4), whatever this names.
3454 #[serde(rename = "pasteType")]
3455 pub paste_type: PasteType,
3456}
3457
3458/// `CopyPasteRequest` — copies `source` to `destination`, spilling a
3459/// larger source past the destination's end or repeating a smaller one to
3460/// fill it, per the Sheets API's own rule.
3461#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3462pub struct CopyPasteRequest {
3463 /// The range to copy from.
3464 pub source: GridRange,
3465 /// The range to copy into.
3466 pub destination: GridRange,
3467 /// What to carry over.
3468 #[serde(rename = "pasteType")]
3469 pub paste_type: PasteType,
3470 /// Whether rows/columns are swapped before pasting.
3471 #[serde(rename = "pasteOrientation")]
3472 pub paste_orientation: PasteOrientation,
3473}
3474
3475/// `PasteDataRequest` — pastes delimited text into a range as if pasted
3476/// from the clipboard, anchored at `coordinate`.
3477///
3478/// `html` is not modelled — this crate's surface is `delimiter`-form only
3479/// (ADR-0083 §4's documented cut), so there is nowhere on this type to put
3480/// markup even if a caller wanted to send it.
3481#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3482pub struct PasteDataRequest {
3483 /// The top-left cell to paste into.
3484 pub coordinate: GridCoordinate,
3485 /// The delimited text to paste.
3486 pub data: String,
3487 /// The delimiter splitting `data` into columns.
3488 pub delimiter: String,
3489 /// What to carry over. `PASTE_NORMAL` on delimited text is undocumented
3490 /// beyond values — ADR-0083 §4 keeps it selectable, resolving both
3491 /// operations, since the API does not say it does nothing extra.
3492 #[serde(rename = "type")]
3493 pub r#type: PasteType,
3494}
3495
3496/// `AutoFillRequest` — the crate's only `autoFill` request (issue #1840,
3497/// [ADR-0083](../../../docs/adrs/adr-0083.md) §1).
3498///
3499/// Extends a series from source cells into an adjacent destination,
3500/// computed entirely by Sheets' own pattern-detection heuristics: this
3501/// crate never sees or predicts the filled *values* — see `auto_fill.rs`'s
3502/// module docs for what `--dry-run` can and cannot say about it.
3503///
3504/// Exactly one of [`Self::range`]/[`Self::source_and_destination`] is set,
3505/// mirroring the API's own oneof; `auto_fill.rs::build_request` is the one
3506/// site that enforces it.
3507#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3508pub struct AutoFillRequest {
3509 /// Form A (`--range`): the whole region. Sheets examines it and
3510 /// decides for itself which cells are the source and which are
3511 /// filled — so, unlike form B, the destination this writes is only
3512 /// ever knowable as an upper bound before the request is sent.
3513 #[serde(skip_serializing_if = "Option::is_none")]
3514 pub range: Option<GridRange>,
3515 /// Form B (`--source`/`--dimension`/`--fill-length`): an explicit
3516 /// source range extended by a caller-chosen length and direction, so
3517 /// the destination is computable client-side.
3518 #[serde(
3519 rename = "sourceAndDestination",
3520 skip_serializing_if = "Option::is_none"
3521 )]
3522 pub source_and_destination: Option<SourceAndDestination>,
3523 /// Fills using the alternate series Sheets would not otherwise choose
3524 /// (`--alternate-series`) — e.g. a copy instead of a linear
3525 /// progression for a plain numeric run, or vice versa. Always sent
3526 /// explicitly, matching this file's other always-present booleans
3527 /// (e.g. [`DimensionGroup::collapsed`]) rather than being omitted when
3528 /// `false`.
3529 #[serde(rename = "useAlternateSeries")]
3530 pub use_alternate_series: bool,
3531}
3532
3533/// `SortRangeRequest` — reorders rows within a bounded grid range.
3534///
3535/// Color-based sort criteria are deliberately not modelled in v1; each
3536/// [`SortSpec`] therefore names a column and ascending/descending order.
3537#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3538pub struct SortRangeRequest {
3539 /// The rows and columns to reorder.
3540 pub range: GridRange,
3541 /// Sort keys in precedence order.
3542 #[serde(rename = "sortSpecs")]
3543 pub sort_specs: Vec<SortSpec>,
3544}
3545
3546/// `RandomizeRangeRequest` — shuffles the row order within a bounded grid
3547/// range into an order chosen server-side.
3548///
3549/// The API's own reply carries nothing this crate can report, so the
3550/// resulting order can never be previewed or read back (issue #1845,
3551/// [ADR-0083](../../../docs/adrs/adr-0083.md) §6).
3552#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3553pub struct RandomizeRangeRequest {
3554 /// The rows and columns to shuffle.
3555 pub range: GridRange,
3556}
3557
3558/// `TextToColumnsRequest` — splits a single column's delimited text across
3559/// the adjacent columns to its right (issue #1843,
3560/// [ADR-0083](../../../docs/adrs/adr-0083.md) §1).
3561///
3562/// The API requires [`Self::source`] to "span exactly one column"; this
3563/// crate never sees or predicts how many columns the split needs — only an
3564/// upper bound computed from a local, non-quote-aware split — see
3565/// `text_to_columns.rs`'s module docs for what `--dry-run` can and cannot
3566/// say about it.
3567#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3568pub struct TextToColumnsRequest {
3569 /// The column to split. Must span exactly one column; may be row-open
3570 /// (`A:A`) or row-bounded (`A2:A100`).
3571 pub source: GridRange,
3572 /// The custom separator. Read by the API only when
3573 /// [`Self::delimiter_type`] is [`DelimiterType::Custom`]; omitted
3574 /// otherwise, matching `AutoFillRequest`'s optional-field convention.
3575 #[serde(skip_serializing_if = "Option::is_none")]
3576 pub delimiter: Option<String>,
3577 /// Which separator to split on, or to detect automatically.
3578 #[serde(rename = "delimiterType")]
3579 pub delimiter_type: DelimiterType,
3580}
3581
3582/// The API's `TextToColumnsRequest.delimiterType` enum.
3583///
3584/// No `DELIMITER_TYPE_UNSPECIFIED` variant is modelled: its behaviour is
3585/// undocumented, so `text_to_columns.rs`'s CLI surface has no way to send
3586/// it and no default that resolves to it.
3587#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
3588pub enum DelimiterType {
3589 /// Split on `,`.
3590 #[serde(rename = "COMMA")]
3591 Comma,
3592 /// Split on `;`.
3593 #[serde(rename = "SEMICOLON")]
3594 Semicolon,
3595 /// Split on `.`.
3596 #[serde(rename = "PERIOD")]
3597 Period,
3598 /// Split on a single space.
3599 #[serde(rename = "SPACE")]
3600 Space,
3601 /// Split on [`TextToColumnsRequest::delimiter`].
3602 #[serde(rename = "CUSTOM")]
3603 Custom,
3604 /// Sheets picks the separator itself.
3605 #[serde(rename = "AUTODETECT")]
3606 Autodetect,
3607}
3608
3609/// `TrimWhitespaceRequest` — trims whitespace in every cell of a range
3610/// (issue #1844, [ADR-0083](../../../docs/adrs/adr-0083.md) §1).
3611///
3612/// The API's exact rule is deliberately not reproduced anywhere in this
3613/// crate: ADR-0083 §6 forbids a preview that predicts *which* cells a trim
3614/// would change, because reproducing the server's rule locally is the
3615/// "preview that lies" risk it declined for `findReplace`. The count of
3616/// cells actually changed comes back from the server in
3617/// [`TrimWhitespaceResponse`].
3618#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3619pub struct TrimWhitespaceRequest {
3620 /// The range whose cells to trim.
3621 pub range: GridRange,
3622}
3623
3624/// `DeleteDuplicatesRequest` — removes rows within a range that duplicate
3625/// an earlier row (issue #1844,
3626/// [ADR-0083](../../../docs/adrs/adr-0083.md) §2).
3627///
3628/// This is the only request in this enum gated by
3629/// [`crate::drive::write_gate::DriveOperation::SheetsDelete`] that is not
3630/// built by `structure.rs`, and the only one anywhere whose removed rows
3631/// are chosen by the **server** rather than named by the caller: the API
3632/// keeps the first instance of each duplicate and removes the rest,
3633/// counting rows with differing case, formatting or formulas as duplicates
3634/// and removing filter-hidden rows along with visible ones.
3635#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3636pub struct DeleteDuplicatesRequest {
3637 /// The range to remove duplicate rows from.
3638 pub range: GridRange,
3639 /// The columns compared for duplicate values. Omitted entirely when
3640 /// empty, which is the API's "analyze all columns in the range"
3641 /// default — sending an empty array would be a different request.
3642 #[serde(rename = "comparisonColumns", skip_serializing_if = "Vec::is_empty")]
3643 pub comparison_columns: Vec<DimensionRange>,
3644}
3645
3646/// [`AutoFillRequest`]'s form-B payload.
3647#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3648pub struct SourceAndDestination {
3649 /// The cells holding the series to extend.
3650 pub source: GridRange,
3651 /// Which axis [`Self::fill_length`] extends along.
3652 pub dimension: Dimension,
3653 /// How many rows/columns to fill, extending from `source`'s edge.
3654 /// **Negative fills backward** (up or left) instead of forward (down
3655 /// or right) — the API's own documented convention. Never `0`;
3656 /// `auto_fill.rs` refuses that before building this request.
3657 #[serde(rename = "fillLength")]
3658 pub fill_length: i64,
3659}
3660
3661/// A typed `findReplace` request. The scope is an enum so callers cannot
3662/// serialise the API's invalid no-scope or multiple-scope shapes.
3663#[allow(missing_docs)]
3664#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3665pub struct FindReplaceRequest {
3666 pub find: String,
3667 pub replacement: String,
3668 #[serde(rename = "matchCase")]
3669 pub match_case: bool,
3670 #[serde(rename = "matchEntireCell")]
3671 pub match_entire_cell: bool,
3672 #[serde(rename = "searchByRegex")]
3673 pub search_by_regex: bool,
3674 #[serde(rename = "includeFormulas")]
3675 pub include_formulas: bool,
3676 #[serde(flatten)]
3677 pub scope: FindReplaceScope,
3678}
3679
3680/// Exactly one `findReplace` scope.
3681#[allow(missing_docs)]
3682#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3683#[serde(untagged)]
3684pub enum FindReplaceScope {
3685 Range {
3686 range: GridRange,
3687 },
3688 Sheet {
3689 #[serde(rename = "sheetId")]
3690 sheet_id: i64,
3691 },
3692 AllSheets {
3693 #[serde(rename = "allSheets")]
3694 all_sheets: bool,
3695 },
3696}
3697
3698/// The `findReplace` reply's exact server counts.
3699#[allow(missing_docs)]
3700#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3701pub struct FindReplaceResponse {
3702 #[serde(default, rename = "valuesChanged")]
3703 pub values_changed: i64,
3704 #[serde(default, rename = "formulasChanged")]
3705 pub formulas_changed: i64,
3706 #[serde(default, rename = "rowsChanged")]
3707 pub rows_changed: i64,
3708 #[serde(default, rename = "sheetsChanged")]
3709 pub sheets_changed: i64,
3710 #[serde(default, rename = "occurrencesChanged")]
3711 pub occurrences_changed: i64,
3712}
3713
3714/// The `trimWhitespace` reply's exact server count (issue #1844).
3715///
3716/// This is the number `trim-whitespace`'s real run reports *instead of*
3717/// re-reading the range: the server knows exactly which cells it changed,
3718/// where `--dry-run` can only name the non-blank cells that might be
3719/// (ADR-0083 §6).
3720#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3721pub struct TrimWhitespaceResponse {
3722 /// The number of cells that were trimmed of whitespace.
3723 #[serde(default, rename = "cellsChangedCount")]
3724 pub cells_changed_count: i64,
3725}
3726
3727/// The `deleteDuplicates` reply's exact server count (issue #1844).
3728///
3729/// The only thing this crate ever learns about *which* rows were removed:
3730/// ADR-0083 §6 places `delete-duplicates` in the server-decided preview
3731/// tier, so neither the dry run nor the real run can enumerate them.
3732#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3733pub struct DeleteDuplicatesResponse {
3734 /// The number of duplicate rows removed.
3735 #[serde(default, rename = "duplicatesRemovedCount")]
3736 pub duplicates_removed_count: i64,
3737}
3738
3739/// Response to `spreadsheets.batchUpdate`.
3740///
3741/// Only `replies` is modelled, and only the `addSheet` arm of it: the new
3742/// sheet's server-assigned `sheetId` is the one fact the response carries
3743/// that the request did not already know, and the request log records it.
3744///
3745/// `PartialEq`-only, not `Eq` since #1797 — see [`BatchUpdateReply`]'s doc
3746/// comment.
3747#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
3748pub struct BatchUpdateResponse {
3749 /// One reply per request, in request order. Replies for requests with
3750 /// nothing to report are empty objects, not omitted.
3751 #[serde(default)]
3752 pub replies: Vec<BatchUpdateReply>,
3753}
3754
3755/// One reply within a [`BatchUpdateResponse`].
3756///
3757/// `PartialEq`-only, not `Eq` (issue #1797): [`AddChartReply`] embeds a
3758/// [`ChartSpec`], which can carry a `PieChartSpec::pie_hole` (`f64`) — the
3759/// same reason [`Spreadsheet`] lost `Eq` in #1793.
3760#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
3761pub struct BatchUpdateReply {
3762 /// Present only for a `findReplace` request (issue #1841).
3763 #[serde(default, rename = "findReplace")]
3764 pub find_replace: Option<FindReplaceResponse>,
3765 /// Present only for an `addSheet` request.
3766 #[serde(default, rename = "addSheet")]
3767 pub add_sheet: Option<AddSheetReply>,
3768 /// Present only for a `duplicateSheet` request. Same wire shape as
3769 /// `addSheet`'s reply (`{"properties": {...}}`), so it reuses
3770 /// [`AddSheetReply`] rather than a near-identical twin type.
3771 #[serde(default, rename = "duplicateSheet")]
3772 pub duplicate_sheet: Option<AddSheetReply>,
3773 /// Present only for an `addProtectedRange` request — carries the
3774 /// server-assigned `protectedRangeId`, only knowable from the reply.
3775 #[serde(default, rename = "addProtectedRange")]
3776 pub add_protected_range: Option<AddProtectedRangeReply>,
3777 /// Present only for an `addFilterView` request — carries the
3778 /// server-assigned `filterViewId`, only knowable from the reply
3779 /// (issue #1794).
3780 #[serde(default, rename = "addFilterView")]
3781 pub add_filter_view: Option<AddFilterViewReply>,
3782 /// Present only for an `addNamedRange` request — carries the
3783 /// server-assigned `namedRangeId`, only knowable from the reply.
3784 #[serde(default, rename = "addNamedRange")]
3785 pub add_named_range: Option<AddNamedRangeReply>,
3786 /// Present only for an `addChart` request — carries the server-assigned
3787 /// `chartId`, only knowable from the reply (issue #1797).
3788 #[serde(default, rename = "addChart")]
3789 pub add_chart: Option<AddChartReply>,
3790 /// Present only for an `addSlicer` request — carries the
3791 /// server-assigned `slicerId`, only knowable from the reply
3792 /// (issue #1797).
3793 #[serde(default, rename = "addSlicer")]
3794 pub add_slicer: Option<AddSlicerReply>,
3795 /// Present only for an `addBanding` request — carries the
3796 /// server-assigned `bandedRangeId`, only knowable from the reply
3797 /// (issue #1832).
3798 #[serde(default, rename = "addBanding")]
3799 pub add_banding: Option<AddBandingReply>,
3800 /// Present only for a `trimWhitespace` request (issue #1844).
3801 #[serde(default, rename = "trimWhitespace")]
3802 pub trim_whitespace: Option<TrimWhitespaceResponse>,
3803 /// Present only for a `deleteDuplicates` request (issue #1844).
3804 #[serde(default, rename = "deleteDuplicates")]
3805 pub delete_duplicates: Option<DeleteDuplicatesResponse>,
3806}
3807
3808/// The `addProtectedRange` arm of a [`BatchUpdateReply`].
3809#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3810pub struct AddProtectedRangeReply {
3811 /// The created protection, including its assigned `protectedRangeId`.
3812 #[serde(default, rename = "protectedRange")]
3813 pub protected_range: Option<ProtectedRange>,
3814}
3815
3816/// The `addFilterView` arm of a [`BatchUpdateReply`] (issue #1794).
3817#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3818pub struct AddFilterViewReply {
3819 /// The created view, including its assigned `filterViewId`.
3820 #[serde(default)]
3821 pub filter: Option<FilterView>,
3822}
3823
3824/// The `addNamedRange` arm of a [`BatchUpdateReply`].
3825#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3826pub struct AddNamedRangeReply {
3827 /// The created named range, including its assigned `namedRangeId`.
3828 #[serde(default, rename = "namedRange")]
3829 pub named_range: Option<NamedRange>,
3830}
3831
3832/// The `addChart` arm of a [`BatchUpdateReply`] (issue #1797).
3833#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
3834pub struct AddChartReply {
3835 /// The created chart, including its assigned `chartId`.
3836 #[serde(default)]
3837 pub chart: Option<EmbeddedChart>,
3838}
3839
3840/// The `addSlicer` arm of a [`BatchUpdateReply`] (issue #1797).
3841#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3842pub struct AddSlicerReply {
3843 /// The created slicer, including its assigned `slicerId`.
3844 #[serde(default)]
3845 pub slicer: Option<Slicer>,
3846}
3847
3848/// The `addBanding` arm of a [`BatchUpdateReply`] (issue #1832).
3849#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
3850pub struct AddBandingReply {
3851 /// The created banded range, including its assigned `bandedRangeId`.
3852 #[serde(default, rename = "bandedRange")]
3853 pub banded_range: Option<BandedRange>,
3854}
3855
3856/// The `addSheet`/`duplicateSheet` arm of a [`BatchUpdateReply`].
3857#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
3858pub struct AddSheetReply {
3859 /// The created sheet's properties, including its assigned `sheetId`.
3860 #[serde(default)]
3861 pub properties: Option<SheetProperties>,
3862}
3863
3864/// The wire literal every developer-metadata request this crate sends
3865/// carries as `visibility` — issue #1795,
3866/// [ADR-0081](../../../docs/adrs/adr-0081.md) §4.
3867///
3868/// `PROJECT`-visibility metadata belongs to whatever OAuth client created
3869/// it and is never this tool's to read or write. `developer_metadata.rs`
3870/// is the only module that constructs this, and it never reads it from a
3871/// flag — there is no `--visibility` option on any of the three CLI
3872/// verbs, so no user input can reach anything but this literal.
3873pub const DOCUMENT_VISIBILITY: &str = "DOCUMENT";
3874
3875/// Where a developer-metadata entry is attached — `DeveloperMetadataLocation`.
3876///
3877/// Sheets models this as a union of `spreadsheet`/`sheetId`/
3878/// `dimensionRange`; exactly one is set by the one function that builds it
3879/// (`developer_metadata.rs::resolve_location`), matching the
3880/// [`ConditionValue`] convention for unions elsewhere in this module —
3881/// discipline enforced by the builder, not the type.
3882#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
3883pub struct DeveloperMetadataLocation {
3884 /// `true` when this entry is attached to the whole spreadsheet.
3885 #[serde(default, skip_serializing_if = "Option::is_none")]
3886 pub spreadsheet: Option<bool>,
3887 /// Set when attached to a whole sheet.
3888 #[serde(default, skip_serializing_if = "Option::is_none", rename = "sheetId")]
3889 pub sheet_id: Option<i64>,
3890 /// Set when attached to a row or column span.
3891 #[serde(
3892 default,
3893 skip_serializing_if = "Option::is_none",
3894 rename = "dimensionRange"
3895 )]
3896 pub dimension_range: Option<DimensionRange>,
3897}
3898
3899impl DeveloperMetadataLocation {
3900 /// A location spanning the whole spreadsheet.
3901 #[must_use]
3902 pub fn spreadsheet() -> Self {
3903 Self {
3904 spreadsheet: Some(true),
3905 ..Self::default()
3906 }
3907 }
3908
3909 /// A location spanning one whole sheet.
3910 #[must_use]
3911 pub fn sheet(sheet_id: i64) -> Self {
3912 Self {
3913 sheet_id: Some(sheet_id),
3914 ..Self::default()
3915 }
3916 }
3917
3918 /// A location spanning a row or column range.
3919 #[must_use]
3920 pub fn dimension(range: DimensionRange) -> Self {
3921 Self {
3922 dimension_range: Some(range),
3923 ..Self::default()
3924 }
3925 }
3926}
3927
3928/// The developer-metadata fields settable on create —
3929/// `CreateDeveloperMetadataRequest.developerMetadata`.
3930///
3931/// No `metadata_id` field (server-assigned) and `visibility` is always
3932/// [`DOCUMENT_VISIBILITY`] — see that constant's doc comment.
3933#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3934pub struct NewDeveloperMetadata {
3935 /// The key this entry is looked up by.
3936 #[serde(rename = "metadataKey")]
3937 pub metadata_key: String,
3938 /// The value stored under `metadata_key`.
3939 #[serde(rename = "metadataValue")]
3940 pub metadata_value: String,
3941 /// Where this entry is attached.
3942 pub location: DeveloperMetadataLocation,
3943 /// Always [`DOCUMENT_VISIBILITY`].
3944 pub visibility: String,
3945}
3946
3947/// `CreateDeveloperMetadataRequest`.
3948#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3949pub struct CreateDeveloperMetadataRequest {
3950 /// The entry to create.
3951 #[serde(rename = "developerMetadata")]
3952 pub developer_metadata: NewDeveloperMetadata,
3953}
3954
3955/// The mutable subset of an existing entry `set-developer-metadata` may
3956/// change on update — currently only its value.
3957///
3958/// Sheets' own `DeveloperMetadata` can also change `metadataKey`/
3959/// `location`/`visibility` via `update`, but this surface never offers
3960/// that: the key and location are how the entry was found, so changing
3961/// them out from under the same request would silently repoint a
3962/// different caller's lookup.
3963#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3964pub struct DeveloperMetadataValueUpdate {
3965 /// The new value.
3966 #[serde(rename = "metadataValue")]
3967 pub metadata_value: String,
3968}
3969
3970/// `UpdateDeveloperMetadataRequest`.
3971///
3972/// Applies to every entry matched by `data_filters` — Sheets bulk-updates
3973/// by filter, not by id, which is what lets `set-developer-metadata`
3974/// resolve "does this key already exist here" and "update it" with the
3975/// same filter, no id round-trip needed.
3976#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3977pub struct UpdateDeveloperMetadataRequest {
3978 /// Selects which existing entries this updates.
3979 #[serde(rename = "dataFilters")]
3980 pub data_filters: Vec<DataFilter>,
3981 /// The new value.
3982 #[serde(rename = "developerMetadata")]
3983 pub developer_metadata: DeveloperMetadataValueUpdate,
3984 /// The field mask limiting what this request may change. Always
3985 /// `"metadataValue"` — the only field [`DeveloperMetadataValueUpdate`]
3986 /// models.
3987 pub fields: String,
3988}
3989
3990/// `DeleteDeveloperMetadataRequest`.
3991///
3992/// **Singular** `dataFilter`, matching the real API — one filter can match
3993/// and delete several entries in a single request, which is why
3994/// `delete-developer-metadata`'s preview lists every match rather than
3995/// assuming exactly one.
3996#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
3997pub struct DeleteDeveloperMetadataRequest {
3998 /// Selects which existing entries this deletes.
3999 #[serde(rename = "dataFilter")]
4000 pub data_filter: DataFilter,
4001}
4002
4003/// A lookup by key and/or location, restricted (by every caller in this
4004/// crate) to [`DOCUMENT_VISIBILITY`] — the `developerMetadataLookup` arm of
4005/// a [`DataFilter`].
4006#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
4007pub struct DeveloperMetadataLookupFilter {
4008 /// Restricts the match to one location, when given.
4009 #[serde(
4010 default,
4011 skip_serializing_if = "Option::is_none",
4012 rename = "metadataLocation"
4013 )]
4014 pub metadata_location: Option<DeveloperMetadataLocation>,
4015 /// Restricts the match to one key, when given.
4016 #[serde(
4017 default,
4018 skip_serializing_if = "Option::is_none",
4019 rename = "metadataKey"
4020 )]
4021 pub metadata_key: Option<String>,
4022 /// Always `Some(DOCUMENT_VISIBILITY)` — every filter this crate builds
4023 /// restricts to document-visibility metadata; see that constant's doc
4024 /// comment.
4025 #[serde(default, skip_serializing_if = "Option::is_none")]
4026 pub visibility: Option<String>,
4027 /// `"EXACT_LOCATION"` when `metadata_location` is given (this crate
4028 /// never sends `"INTERSECTING_LOCATION"`, which would also match a
4029 /// dimension range straddling the requested one).
4030 #[serde(
4031 default,
4032 skip_serializing_if = "Option::is_none",
4033 rename = "locationMatchingStrategy"
4034 )]
4035 pub location_matching_strategy: Option<String>,
4036}
4037
4038/// `DataFilter`, restricted to the one shape this crate ever sends: a
4039/// `developerMetadataLookup`.
4040///
4041/// Sheets' `DataFilter` also has `a1Range`/`gridRange` alternatives, never
4042/// constructed here — the same curated, not full-coverage, stance as
4043/// [`BooleanCondition`]'s doc comment.
4044#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
4045pub struct DataFilter {
4046 /// The lookup this filter applies.
4047 #[serde(rename = "developerMetadataLookup")]
4048 pub developer_metadata_lookup: DeveloperMetadataLookupFilter,
4049}
4050
4051/// One developer-metadata entry as read back from the server —
4052/// `DeveloperMetadata`, the response/search shape (has an id; the create
4053/// payload, [`NewDeveloperMetadata`], does not).
4054#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
4055pub struct DeveloperMetadata {
4056 /// The server-assigned id.
4057 #[serde(default, rename = "metadataId")]
4058 pub metadata_id: i64,
4059 /// The key this entry is looked up by.
4060 #[serde(default, rename = "metadataKey")]
4061 pub metadata_key: String,
4062 /// The value stored under `metadata_key`.
4063 #[serde(default, rename = "metadataValue")]
4064 pub metadata_value: String,
4065 /// Where this entry is attached.
4066 #[serde(default)]
4067 pub location: DeveloperMetadataLocation,
4068 /// The entry's visibility. `developer_metadata.rs` asserts this is
4069 /// always [`DOCUMENT_VISIBILITY`] on every entry it reads — the
4070 /// defense-in-depth half of the DOCUMENT-only guarantee, on top of
4071 /// every request already restricting the search itself (issue #1795,
4072 /// ADR-0081 §4).
4073 #[serde(default)]
4074 pub visibility: String,
4075}
4076
4077/// Body of `spreadsheets.developerMetadata:search`.
4078#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
4079pub struct SearchDeveloperMetadataRequest {
4080 /// Every filter must match (Sheets ANDs a multi-entry list); this crate
4081 /// always sends exactly one.
4082 #[serde(rename = "dataFilters")]
4083 pub data_filters: Vec<DataFilter>,
4084}
4085
4086/// Response to `spreadsheets.developerMetadata:search`.
4087#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
4088pub struct SearchDeveloperMetadataResponse {
4089 /// Every entry that matched, unwrapped from its `MatchedDeveloperMetadata`
4090 /// wrapper (the echoed `dataFilters` that wrapper also carries are not
4091 /// modelled — this crate already knows which filter it sent).
4092 #[serde(default, rename = "matchedDeveloperMetadata")]
4093 pub matched_developer_metadata: Vec<MatchedDeveloperMetadata>,
4094}
4095
4096/// One match within a [`SearchDeveloperMetadataResponse`].
4097#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
4098pub struct MatchedDeveloperMetadata {
4099 /// The matched entry.
4100 #[serde(rename = "developerMetadata")]
4101 pub developer_metadata: DeveloperMetadata,
4102}
4103
4104#[cfg(test)]
4105#[allow(clippy::unwrap_used, clippy::expect_used)]
4106mod tests {
4107 use super::*;
4108
4109 #[test]
4110 fn shift_dimension_as_str_matches_the_wire_spelling() {
4111 assert_eq!(ShiftDimension::Rows.as_str(), "ROWS");
4112 assert_eq!(ShiftDimension::Columns.as_str(), "COLUMNS");
4113 }
4114
4115 #[test]
4116 fn recalculation_interval_as_str_matches_the_wire_spelling() {
4117 assert_eq!(
4118 RecalculationInterval::Unspecified.as_str(),
4119 "RECALCULATION_INTERVAL_UNSPECIFIED"
4120 );
4121 assert_eq!(RecalculationInterval::OnChange.as_str(), "ON_CHANGE");
4122 assert_eq!(RecalculationInterval::Minute.as_str(), "MINUTE");
4123 assert_eq!(RecalculationInterval::Hour.as_str(), "HOUR");
4124 }
4125
4126 #[test]
4127 fn paste_type_as_str_matches_the_wire_spelling() {
4128 assert_eq!(PasteType::Normal.as_str(), "PASTE_NORMAL");
4129 assert_eq!(PasteType::Values.as_str(), "PASTE_VALUES");
4130 assert_eq!(PasteType::Formula.as_str(), "PASTE_FORMULA");
4131 assert_eq!(PasteType::Format.as_str(), "PASTE_FORMAT");
4132 }
4133
4134 #[test]
4135 fn paste_orientation_as_str_matches_the_wire_spelling() {
4136 assert_eq!(PasteOrientation::Normal.as_str(), "NORMAL");
4137 assert_eq!(PasteOrientation::Transpose.as_str(), "TRANSPOSE");
4138 }
4139
4140 #[test]
4141 fn iterative_calculation_settings_describe_bounds_covers_every_combination() {
4142 assert_eq!(
4143 IterativeCalculationSettings {
4144 max_iterations: Some(50),
4145 convergence_threshold: Some(0.01),
4146 }
4147 .describe_bounds(),
4148 " (max 50 iterations, threshold 0.01)"
4149 );
4150 assert_eq!(
4151 IterativeCalculationSettings {
4152 max_iterations: Some(50),
4153 convergence_threshold: None,
4154 }
4155 .describe_bounds(),
4156 " (max 50 iterations)"
4157 );
4158 assert_eq!(
4159 IterativeCalculationSettings {
4160 max_iterations: None,
4161 convergence_threshold: Some(0.01),
4162 }
4163 .describe_bounds(),
4164 " (threshold 0.01)"
4165 );
4166 assert_eq!(
4167 IterativeCalculationSettings::default().describe_bounds(),
4168 ""
4169 );
4170 }
4171
4172 #[test]
4173 fn insert_range_serializes_the_column_shift_direction() {
4174 let request = BatchUpdateRequestItem::InsertRange(InsertRangeRequest {
4175 range: GridRange {
4176 sheet_id: 7,
4177 start_row_index: Some(1),
4178 end_row_index: Some(2),
4179 start_column_index: Some(3),
4180 end_column_index: Some(4),
4181 },
4182 shift_dimension: ShiftDimension::Columns,
4183 });
4184 assert_eq!(
4185 serde_json::to_value(request).unwrap(),
4186 serde_json::json!({
4187 "insertRange": {
4188 "range": {
4189 "sheetId": 7,
4190 "startRowIndex": 1,
4191 "endRowIndex": 2,
4192 "startColumnIndex": 3,
4193 "endColumnIndex": 4,
4194 },
4195 "shiftDimension": "COLUMNS",
4196 },
4197 })
4198 );
4199 }
4200
4201 #[test]
4202 fn cut_paste_request_serializes_the_paste_type() {
4203 let request = BatchUpdateRequestItem::CutPaste(CutPasteRequest {
4204 source: GridRange {
4205 sheet_id: 1,
4206 start_row_index: Some(0),
4207 end_row_index: Some(2),
4208 start_column_index: Some(0),
4209 end_column_index: Some(2),
4210 },
4211 destination: GridCoordinate {
4212 sheet_id: 1,
4213 row_index: 5,
4214 column_index: 5,
4215 },
4216 paste_type: PasteType::Values,
4217 });
4218 assert_eq!(
4219 serde_json::to_value(request).unwrap(),
4220 serde_json::json!({
4221 "cutPaste": {
4222 "source": {
4223 "sheetId": 1,
4224 "startRowIndex": 0,
4225 "endRowIndex": 2,
4226 "startColumnIndex": 0,
4227 "endColumnIndex": 2,
4228 },
4229 "destination": {"sheetId": 1, "rowIndex": 5, "columnIndex": 5},
4230 "pasteType": "PASTE_VALUES",
4231 },
4232 })
4233 );
4234 }
4235
4236 #[test]
4237 fn copy_paste_request_serializes_type_and_orientation() {
4238 let request = BatchUpdateRequestItem::CopyPaste(CopyPasteRequest {
4239 source: GridRange {
4240 sheet_id: 1,
4241 start_row_index: Some(0),
4242 end_row_index: Some(1),
4243 start_column_index: Some(0),
4244 end_column_index: Some(1),
4245 },
4246 destination: GridRange {
4247 sheet_id: 1,
4248 start_row_index: Some(5),
4249 end_row_index: Some(6),
4250 start_column_index: Some(5),
4251 end_column_index: Some(6),
4252 },
4253 paste_type: PasteType::Normal,
4254 paste_orientation: PasteOrientation::Transpose,
4255 });
4256 assert_eq!(
4257 serde_json::to_value(request).unwrap(),
4258 serde_json::json!({
4259 "copyPaste": {
4260 "source": {
4261 "sheetId": 1, "startRowIndex": 0, "endRowIndex": 1,
4262 "startColumnIndex": 0, "endColumnIndex": 1,
4263 },
4264 "destination": {
4265 "sheetId": 1, "startRowIndex": 5, "endRowIndex": 6,
4266 "startColumnIndex": 5, "endColumnIndex": 6,
4267 },
4268 "pasteType": "PASTE_NORMAL",
4269 "pasteOrientation": "TRANSPOSE",
4270 },
4271 })
4272 );
4273 }
4274
4275 #[test]
4276 fn paste_data_request_serializes_the_delimiter_form() {
4277 let request = BatchUpdateRequestItem::PasteData(PasteDataRequest {
4278 coordinate: GridCoordinate {
4279 sheet_id: 2,
4280 row_index: 0,
4281 column_index: 0,
4282 },
4283 data: "a\tb\nc\td".to_string(),
4284 delimiter: "\t".to_string(),
4285 r#type: PasteType::Values,
4286 });
4287 assert_eq!(
4288 serde_json::to_value(request).unwrap(),
4289 serde_json::json!({
4290 "pasteData": {
4291 "coordinate": {"sheetId": 2, "rowIndex": 0, "columnIndex": 0},
4292 "data": "a\tb\nc\td",
4293 "delimiter": "\t",
4294 "type": "PASTE_VALUES",
4295 },
4296 })
4297 );
4298 }
4299
4300 #[test]
4301 fn paste_type_writes_values_and_presentation_match_the_adr_0083_4_table() {
4302 assert!(PasteType::Normal.writes_values());
4303 assert!(PasteType::Normal.writes_presentation());
4304 assert!(PasteType::Values.writes_values());
4305 assert!(!PasteType::Values.writes_presentation());
4306 assert!(PasteType::Formula.writes_values());
4307 assert!(!PasteType::Formula.writes_presentation());
4308 assert!(!PasteType::Format.writes_values());
4309 assert!(PasteType::Format.writes_presentation());
4310 }
4311
4312 #[test]
4313 fn auto_fill_serializes_the_range_form_with_no_source_and_destination_key() {
4314 let request = BatchUpdateRequestItem::AutoFill(AutoFillRequest {
4315 range: Some(GridRange {
4316 sheet_id: 0,
4317 start_row_index: Some(0),
4318 end_row_index: Some(10),
4319 start_column_index: Some(0),
4320 end_column_index: Some(1),
4321 }),
4322 source_and_destination: None,
4323 use_alternate_series: false,
4324 });
4325 assert_eq!(
4326 serde_json::to_value(request).unwrap(),
4327 serde_json::json!({
4328 "autoFill": {
4329 "range": {
4330 "sheetId": 0,
4331 "startRowIndex": 0,
4332 "endRowIndex": 10,
4333 "startColumnIndex": 0,
4334 "endColumnIndex": 1,
4335 },
4336 "useAlternateSeries": false,
4337 },
4338 })
4339 );
4340 }
4341
4342 #[test]
4343 fn auto_fill_serializes_the_source_and_destination_form_with_no_range_key() {
4344 let request = BatchUpdateRequestItem::AutoFill(AutoFillRequest {
4345 range: None,
4346 source_and_destination: Some(SourceAndDestination {
4347 source: GridRange {
4348 sheet_id: 0,
4349 start_row_index: Some(0),
4350 end_row_index: Some(3),
4351 start_column_index: Some(0),
4352 end_column_index: Some(1),
4353 },
4354 dimension: Dimension::Rows,
4355 fill_length: -2,
4356 }),
4357 use_alternate_series: true,
4358 });
4359 assert_eq!(
4360 serde_json::to_value(request).unwrap(),
4361 serde_json::json!({
4362 "autoFill": {
4363 "sourceAndDestination": {
4364 "source": {
4365 "sheetId": 0,
4366 "startRowIndex": 0,
4367 "endRowIndex": 3,
4368 "startColumnIndex": 0,
4369 "endColumnIndex": 1,
4370 },
4371 "dimension": "ROWS",
4372 "fillLength": -2,
4373 },
4374 "useAlternateSeries": true,
4375 },
4376 })
4377 );
4378 }
4379
4380 #[test]
4381 fn sort_range_serializes_its_range_and_ordered_specs() {
4382 let request = BatchUpdateRequestItem::SortRange(SortRangeRequest {
4383 range: GridRange {
4384 sheet_id: 0,
4385 start_row_index: Some(1),
4386 end_row_index: Some(10),
4387 start_column_index: Some(0),
4388 end_column_index: Some(3),
4389 },
4390 sort_specs: vec![
4391 SortSpec {
4392 dimension_index: 1,
4393 sort_order: SortOrder::Descending,
4394 },
4395 SortSpec {
4396 dimension_index: 0,
4397 sort_order: SortOrder::Ascending,
4398 },
4399 ],
4400 });
4401 assert_eq!(
4402 serde_json::to_value(request).unwrap(),
4403 serde_json::json!({
4404 "sortRange": {
4405 "range": {
4406 "sheetId": 0, "startRowIndex": 1, "endRowIndex": 10,
4407 "startColumnIndex": 0, "endColumnIndex": 3,
4408 },
4409 "sortSpecs": [
4410 {"dimensionIndex": 1, "sortOrder": "DESCENDING"},
4411 {"dimensionIndex": 0, "sortOrder": "ASCENDING"},
4412 ],
4413 },
4414 })
4415 );
4416 }
4417
4418 #[test]
4419 fn randomize_range_serializes_its_range() {
4420 let request = BatchUpdateRequestItem::RandomizeRange(RandomizeRangeRequest {
4421 range: GridRange {
4422 sheet_id: 0,
4423 start_row_index: Some(1),
4424 end_row_index: Some(10),
4425 start_column_index: Some(0),
4426 end_column_index: Some(3),
4427 },
4428 });
4429 assert_eq!(
4430 serde_json::to_value(request).unwrap(),
4431 serde_json::json!({
4432 "randomizeRange": {
4433 "range": {
4434 "sheetId": 0, "startRowIndex": 1, "endRowIndex": 10,
4435 "startColumnIndex": 0, "endColumnIndex": 3,
4436 },
4437 },
4438 })
4439 );
4440 }
4441
4442 #[test]
4443 fn text_to_columns_omits_delimiter_for_a_fixed_type() {
4444 let request = BatchUpdateRequestItem::TextToColumns(TextToColumnsRequest {
4445 source: GridRange {
4446 sheet_id: 0,
4447 start_row_index: Some(1),
4448 end_row_index: Some(100),
4449 start_column_index: Some(0),
4450 end_column_index: Some(1),
4451 },
4452 delimiter: None,
4453 delimiter_type: DelimiterType::Comma,
4454 });
4455 assert_eq!(
4456 serde_json::to_value(request).unwrap(),
4457 serde_json::json!({
4458 "textToColumns": {
4459 "source": {
4460 "sheetId": 0, "startRowIndex": 1, "endRowIndex": 100,
4461 "startColumnIndex": 0, "endColumnIndex": 1,
4462 },
4463 "delimiterType": "COMMA",
4464 },
4465 })
4466 );
4467 }
4468
4469 #[test]
4470 fn text_to_columns_serializes_a_custom_delimiter() {
4471 let request = BatchUpdateRequestItem::TextToColumns(TextToColumnsRequest {
4472 source: GridRange {
4473 sheet_id: 0,
4474 start_row_index: Some(0),
4475 end_row_index: Some(5),
4476 start_column_index: Some(0),
4477 end_column_index: Some(1),
4478 },
4479 delimiter: Some("|".to_string()),
4480 delimiter_type: DelimiterType::Custom,
4481 });
4482 assert_eq!(
4483 serde_json::to_value(request).unwrap(),
4484 serde_json::json!({
4485 "textToColumns": {
4486 "source": {
4487 "sheetId": 0, "startRowIndex": 0, "endRowIndex": 5,
4488 "startColumnIndex": 0, "endColumnIndex": 1,
4489 },
4490 "delimiter": "|",
4491 "delimiterType": "CUSTOM",
4492 },
4493 })
4494 );
4495 }
4496
4497 #[test]
4498 fn spreadsheet_parses_a_realistic_fields_masked_response() {
4499 let json = serde_json::json!({
4500 "spreadsheetId": "sheet-1",
4501 "properties": {"title": "Budget"},
4502 "sheets": [
4503 {"properties": {"sheetId": 0, "title": "Q1", "index": 0,
4504 "gridProperties": {"rowCount": 1000, "columnCount": 26}}},
4505 {"properties": {"sheetId": 7, "title": "My Sheet", "index": 1, "hidden": true}},
4506 ],
4507 });
4508 let parsed: Spreadsheet = serde_json::from_value(json).unwrap();
4509 assert_eq!(parsed.title(), "Budget");
4510 assert_eq!(parsed.sheet_titles(), vec!["Q1", "My Sheet"]);
4511 assert!(!parsed.sheets[0].hidden());
4512 assert!(parsed.sheets[1].hidden());
4513 assert_eq!(
4514 parsed.sheets[0]
4515 .properties
4516 .as_ref()
4517 .unwrap()
4518 .grid_properties
4519 .as_ref()
4520 .unwrap()
4521 .row_count,
4522 Some(1000)
4523 );
4524 }
4525
4526 #[test]
4527 fn spreadsheet_tolerates_unmodelled_fields() {
4528 let json = serde_json::json!({
4529 "spreadsheetId": "s",
4530 "somethingNew": {"nested": true},
4531 "sheets": [{"properties": {"title": "A"}, "somethingElseNew": 1}],
4532 });
4533 let parsed: Spreadsheet = serde_json::from_value(json).unwrap();
4534 assert_eq!(parsed.sheet_titles(), vec!["A"]);
4535 }
4536
4537 #[test]
4538 fn sheet_conditional_formats_round_trips_a_boolean_and_a_gradient_rule() {
4539 // issue #1793 — `conditionalFormats` used to be dropped as an
4540 // unmodelled field (see the fixture `spreadsheet_tolerates_unmodelled_fields`
4541 // predates this test with); confirm it's now real, in both shapes.
4542 let json = serde_json::json!({
4543 "sheets": [{
4544 "properties": {"title": "A"},
4545 "conditionalFormats": [
4546 {
4547 "ranges": [{"sheetId": 0, "startRowIndex": 0, "endRowIndex": 1}],
4548 "booleanRule": {
4549 "condition": {"type": "NUMBER_GREATER", "values": [{"userEnteredValue": "10"}]},
4550 "format": {"backgroundColorStyle": {"rgbColor": {"red": 1.0, "green": 0.0, "blue": 0.0}}},
4551 },
4552 },
4553 {
4554 "ranges": [{"sheetId": 0, "startRowIndex": 1, "endRowIndex": 2}],
4555 // The real read shape (issue #1944): each point an
4556 // `InterpolationPoint`, the deprecated `color` echoed
4557 // beside `colorStyle`, and no `value` on a `MIN`/`MAX`
4558 // point. A UI-made rule can anchor an endpoint at a
4559 // `NUMBER`, which this crate never builds.
4560 "gradientRule": {
4561 "minpoint": {
4562 "color": {"red": 1.0, "green": 1.0, "blue": 1.0},
4563 "colorStyle": {"rgbColor": {"red": 1.0, "green": 1.0, "blue": 1.0}},
4564 "type": "MIN",
4565 },
4566 "maxpoint": {
4567 "color": {"red": 0.0, "green": 1.0, "blue": 0.0},
4568 "colorStyle": {"rgbColor": {"red": 0.0, "green": 1.0, "blue": 0.0}},
4569 "type": "NUMBER",
4570 "value": "100",
4571 },
4572 },
4573 },
4574 ],
4575 }],
4576 });
4577 let parsed: Spreadsheet = serde_json::from_value(json).unwrap();
4578 let rules = &parsed.sheets[0].conditional_formats;
4579 assert_eq!(rules.len(), 2);
4580 assert_eq!(
4581 rules[0]
4582 .boolean_rule
4583 .as_ref()
4584 .unwrap()
4585 .condition
4586 .condition_type,
4587 "NUMBER_GREATER"
4588 );
4589 assert!(rules[0].gradient_rule.is_none());
4590 assert!(rules[1].boolean_rule.is_none());
4591 let gradient = rules[1].gradient_rule.as_ref().unwrap();
4592 assert!(gradient.midpoint.is_none());
4593 let min = gradient.minpoint.as_ref().unwrap();
4594 assert_eq!(min.point_type, "MIN");
4595 assert_eq!(min.value, None);
4596 assert_eq!(
4597 min.color_style.rgb_color,
4598 Some(Color {
4599 red: 1.0,
4600 green: 1.0,
4601 blue: 1.0
4602 })
4603 );
4604 let max = gradient.maxpoint.as_ref().unwrap();
4605 assert_eq!(max.point_type, "NUMBER");
4606 assert_eq!(max.value.as_deref(), Some("100"));
4607 }
4608
4609 #[test]
4610 fn spreadsheet_title_is_empty_when_the_fields_mask_omitted_it() {
4611 let parsed: Spreadsheet = serde_json::from_value(serde_json::json!({})).unwrap();
4612 assert_eq!(parsed.title(), "");
4613 assert!(parsed.sheet_titles().is_empty());
4614 }
4615
4616 #[test]
4617 fn has_sheet_titled_skips_a_property_less_sheet() {
4618 // A sheet with no `properties` must never match, not even "" — it
4619 // has no title at all, unlike `Sheet::title()`'s `""` display
4620 // fallback (issue #1702).
4621 let json = serde_json::json!({
4622 "sheets": [
4623 {},
4624 {"properties": {"title": "Q1"}},
4625 ],
4626 });
4627 let parsed: Spreadsheet = serde_json::from_value(json).unwrap();
4628 assert!(!parsed.has_sheet_titled(""));
4629 assert!(parsed.has_sheet_titled("Q1"));
4630 assert!(!parsed.has_sheet_titled("nonexistent"));
4631 }
4632
4633 #[test]
4634 fn value_range_absent_values_parses_as_empty_not_an_error() {
4635 // The shape an empty sheet actually returns: `values` is missing.
4636 let json = serde_json::json!({"range": "'Blank'!A1:Z1000"});
4637 let parsed: ValueRange = serde_json::from_value(json).unwrap();
4638 assert!(parsed.values.is_empty());
4639 assert_eq!(parsed.width(), 0);
4640 assert_eq!(parsed.range.as_deref(), Some("'Blank'!A1:Z1000"));
4641 }
4642
4643 #[test]
4644 fn value_range_width_is_the_widest_ragged_row() {
4645 let json = serde_json::json!({
4646 "range": "S!A1:C2",
4647 "values": [["a", "b", "c"], ["d"]],
4648 });
4649 let parsed: ValueRange = serde_json::from_value(json).unwrap();
4650 assert_eq!(parsed.width(), 3);
4651 assert_eq!(parsed.values[1].len(), 1, "raggedness is preserved as-is");
4652 }
4653
4654 #[test]
4655 fn value_range_preserves_non_string_cell_types() {
4656 // UNFORMATTED_VALUE yields JSON numbers and bools, not strings.
4657 let json = serde_json::json!({"range": "S!A1:C1", "values": [[1234.5, true, "x"]]});
4658 let parsed: ValueRange = serde_json::from_value(json).unwrap();
4659 assert!(parsed.values[0][0].is_number());
4660 assert!(parsed.values[0][1].is_boolean());
4661 assert!(parsed.values[0][2].is_string());
4662 }
4663
4664 #[test]
4665 fn batch_get_response_parses_and_defaults_to_empty() {
4666 let json = serde_json::json!({
4667 "valueRanges": [
4668 {"range": "'A'!A1:B1", "values": [["1", "2"]]},
4669 {"range": "'B'!A1:A1"},
4670 ],
4671 });
4672 let parsed: BatchGetValuesResponse = serde_json::from_value(json).unwrap();
4673 assert_eq!(parsed.value_ranges.len(), 2);
4674 assert!(parsed.value_ranges[1].values.is_empty());
4675
4676 let empty: BatchGetValuesResponse = serde_json::from_value(serde_json::json!({})).unwrap();
4677 assert!(empty.value_ranges.is_empty());
4678 }
4679 #[test]
4680 fn update_values_response_parses_the_counts() {
4681 let json = serde_json::json!({
4682 "spreadsheetId": "s", "updatedRange": "'Q1'!A1:B2",
4683 "updatedRows": 2, "updatedColumns": 2, "updatedCells": 4,
4684 });
4685 let parsed: UpdateValuesResponse = serde_json::from_value(json).unwrap();
4686 assert_eq!(parsed.updated_range.as_deref(), Some("'Q1'!A1:B2"));
4687 assert_eq!(parsed.updated_cells, Some(4));
4688 }
4689
4690 #[test]
4691 fn append_values_response_nests_the_counts_under_updates() {
4692 // The one shape difference from values.update — reading the counts
4693 // from the top level here would silently log zeroes.
4694 let json = serde_json::json!({
4695 "tableRange": "'Q1'!A1:B3",
4696 "updates": {"updatedRange": "'Q1'!A4:B4", "updatedRows": 1, "updatedCells": 2},
4697 });
4698 let parsed: AppendValuesResponse = serde_json::from_value(json).unwrap();
4699 assert_eq!(parsed.table_range.as_deref(), Some("'Q1'!A1:B3"));
4700 assert_eq!(parsed.updates.unwrap().updated_cells, Some(2));
4701 }
4702
4703 #[test]
4704 fn clear_values_response_parses_the_cleared_range() {
4705 let json = serde_json::json!({"spreadsheetId": "s", "clearedRange": "'Q1'!A1:Z999"});
4706 let parsed: ClearValuesResponse = serde_json::from_value(json).unwrap();
4707 assert_eq!(parsed.cleared_range.as_deref(), Some("'Q1'!A1:Z999"));
4708 }
4709
4710 #[test]
4711 fn write_responses_tolerate_missing_counts() {
4712 let parsed: UpdateValuesResponse = serde_json::from_value(serde_json::json!({})).unwrap();
4713 assert_eq!(parsed.updated_cells, None);
4714 let parsed: AppendValuesResponse = serde_json::from_value(serde_json::json!({})).unwrap();
4715 assert!(parsed.updates.is_none());
4716 }
4717
4718 #[test]
4719 fn find_replace_request_serializes_exactly_one_scope_and_reply_counts() {
4720 let request = BatchUpdateRequestItem::FindReplace(FindReplaceRequest {
4721 find: "draft".to_string(),
4722 replacement: "final".to_string(),
4723 match_case: true,
4724 match_entire_cell: false,
4725 search_by_regex: false,
4726 include_formulas: true,
4727 scope: FindReplaceScope::Sheet { sheet_id: 0 },
4728 });
4729 assert_eq!(
4730 serde_json::to_value(request).unwrap(),
4731 serde_json::json!({"findReplace": {
4732 "find": "draft", "replacement": "final", "matchCase": true,
4733 "matchEntireCell": false, "searchByRegex": false,
4734 "includeFormulas": true, "sheetId": 0,
4735 }})
4736 );
4737 let response: BatchUpdateResponse = serde_json::from_value(serde_json::json!({
4738 "replies": [{"findReplace": {"valuesChanged": 2, "formulasChanged": 3,
4739 "rowsChanged": 4, "sheetsChanged": 5, "occurrencesChanged": 6}}]
4740 }))
4741 .unwrap();
4742 assert_eq!(
4743 response.replies[0]
4744 .find_replace
4745 .as_ref()
4746 .unwrap()
4747 .occurrences_changed,
4748 6
4749 );
4750 }
4751
4752 // ── issue #1929: proto3 zero-value omission ─────────────────────────
4753 //
4754 // Sheets omits a scalar field from its JSON entirely when it holds
4755 // proto3's default (`0`, `0.0`, `false`, `""`) — see the module doc.
4756 // Each test below feeds exactly that omitted shape to the struct that
4757 // broke on it live, and asserts the zero default is recovered rather
4758 // than the parse failing.
4759
4760 #[test]
4761 fn grid_coordinate_defaults_every_field_when_all_are_zero() {
4762 // sheet 0, row 1 (index 0), column A (index 0) — add-chart's own
4763 // live failure (issue #1929).
4764 let parsed: GridCoordinate = serde_json::from_value(serde_json::json!({})).unwrap();
4765 assert_eq!(parsed.sheet_id, 0);
4766 assert_eq!(parsed.row_index, 0);
4767 assert_eq!(parsed.column_index, 0);
4768 }
4769
4770 #[test]
4771 fn dimension_range_defaults_sheet_and_start_index() {
4772 let parsed: DimensionRange = serde_json::from_value(serde_json::json!({
4773 "dimension": "ROWS",
4774 "endIndex": 3,
4775 }))
4776 .unwrap();
4777 assert_eq!(parsed.sheet_id, 0);
4778 assert_eq!(parsed.start_index, 0);
4779 assert_eq!(parsed.end_index, 3);
4780 assert_eq!(parsed.dimension, Dimension::Rows);
4781 }
4782
4783 #[test]
4784 fn color_defaults_the_channels_it_wasnt_given() {
4785 // Struct-level comparison, not a direct float `==`, so this doesn't
4786 // trip clippy's `float_cmp` — see `Color`'s own doc comment on why
4787 // it has no meaningful `Eq`.
4788 let parsed: Color = serde_json::from_value(serde_json::json!({"red": 1.0})).unwrap();
4789 assert_eq!(
4790 parsed,
4791 Color {
4792 red: 1.0,
4793 green: 0.0,
4794 blue: 0.0,
4795 }
4796 );
4797 }
4798
4799 #[test]
4800 fn color_style_defaults_an_all_omitted_rgb_color() {
4801 let parsed: ColorStyle =
4802 serde_json::from_value(serde_json::json!({"rgbColor": {}})).unwrap();
4803 assert_eq!(parsed.rgb_color, Some(Color::default()));
4804 assert_eq!(parsed.theme_color, None);
4805 }
4806
4807 #[test]
4808 fn color_style_reads_a_theme_color_only_arm() {
4809 let parsed: ColorStyle =
4810 serde_json::from_value(serde_json::json!({"themeColor": "ACCENT1"})).unwrap();
4811 assert_eq!(parsed.rgb_color, None);
4812 assert_eq!(parsed.theme_color, Some("ACCENT1".to_string()));
4813 }
4814
4815 #[test]
4816 fn color_style_reads_an_empty_object_as_neither_arm() {
4817 let parsed: ColorStyle = serde_json::from_value(serde_json::json!({})).unwrap();
4818 assert_eq!(parsed.rgb_color, None);
4819 assert_eq!(parsed.theme_color, None);
4820 }
4821
4822 #[test]
4823 fn color_style_round_trips_a_theme_color_unchanged() {
4824 let original = serde_json::json!({"themeColor": "ACCENT1"});
4825 let parsed: ColorStyle = serde_json::from_value(original.clone()).unwrap();
4826 assert_eq!(serde_json::to_value(&parsed).unwrap(), original);
4827 }
4828
4829 #[test]
4830 fn color_style_rgb_serializes_only_rgb_color() {
4831 let style = ColorStyle::rgb(Color {
4832 red: 1.0,
4833 green: 0.0,
4834 blue: 0.0,
4835 });
4836 let value = serde_json::to_value(&style).unwrap();
4837 assert_eq!(
4838 value,
4839 serde_json::json!({"rgbColor": {"red": 1.0, "green": 0.0, "blue": 0.0}}),
4840 "themeColor must not appear on an outbound rgb ColorStyle"
4841 );
4842 }
4843
4844 #[test]
4845 fn color_style_describe_prefers_rgb_over_theme() {
4846 let style = ColorStyle::rgb(Color {
4847 red: 1.0,
4848 green: 0.0,
4849 blue: 0.0,
4850 });
4851 assert_eq!(style.describe(), "#FF0000");
4852 }
4853
4854 #[test]
4855 fn color_style_describe_names_a_theme_color() {
4856 let style = ColorStyle::theme("ACCENT1");
4857 assert_eq!(style.describe(), "theme:ACCENT1");
4858 }
4859
4860 #[test]
4861 fn color_style_describe_reports_neither_arm_as_unset() {
4862 assert_eq!(ColorStyle::default().describe(), "(unset)");
4863 }
4864
4865 #[test]
4866 fn gradient_rule_parses_a_theme_color_only_minpoint() {
4867 // `InterpolationPoint.color_style` is required (non-`Option`), so
4868 // this is the one surface where a theme-only `ColorStyle` sits
4869 // directly on a required field rather than behind an `Option`
4870 // (issue #2020).
4871 let parsed: GradientRule = serde_json::from_value(serde_json::json!({
4872 "minpoint": {"colorStyle": {"themeColor": "ACCENT1"}, "type": "MIN"},
4873 "maxpoint": {"colorStyle": {"rgbColor": {"red": 1.0}}, "type": "MAX"},
4874 }))
4875 .unwrap();
4876 let minpoint = parsed.minpoint.unwrap();
4877 assert_eq!(minpoint.color_style.rgb_color, None);
4878 assert_eq!(
4879 minpoint.color_style.theme_color,
4880 Some("ACCENT1".to_string())
4881 );
4882 }
4883
4884 #[test]
4885 fn pivot_group_defaults_offset_and_totals() {
4886 // A grouping by the source's first column, with `--no-totals`.
4887 let parsed: PivotGroup = serde_json::from_value(serde_json::json!({})).unwrap();
4888 assert_eq!(parsed.source_column_offset, 0);
4889 assert!(!parsed.show_totals);
4890 assert_eq!(parsed.sort_order, None);
4891 }
4892
4893 #[test]
4894 fn pivot_value_defaults_the_source_column_offset() {
4895 let parsed: PivotValue =
4896 serde_json::from_value(serde_json::json!({"summarizeFunction": "SUM"})).unwrap();
4897 assert_eq!(parsed.source_column_offset, 0);
4898 assert_eq!(parsed.summarize_function, "SUM");
4899 }
4900
4901 #[test]
4902 fn pivot_filter_spec_defaults_the_column_offset_index() {
4903 let parsed: PivotFilterSpec =
4904 serde_json::from_value(serde_json::json!({"filterCriteria": {}})).unwrap();
4905 assert_eq!(parsed.column_offset_index, 0);
4906 assert!(parsed.filter_criteria.visible_values.is_empty());
4907 }
4908
4909 #[test]
4910 fn sort_spec_defaults_the_dimension_index_to_column_a() {
4911 let parsed: SortSpec =
4912 serde_json::from_value(serde_json::json!({"sortOrder": "ASCENDING"})).unwrap();
4913 assert_eq!(parsed.dimension_index, 0);
4914 assert_eq!(parsed.sort_order, SortOrder::Ascending);
4915 }
4916
4917 #[test]
4918 fn number_format_defaults_an_empty_pattern() {
4919 let parsed: NumberFormat =
4920 serde_json::from_value(serde_json::json!({"type": "NUMBER"})).unwrap();
4921 assert_eq!(parsed.format_type, "NUMBER");
4922 assert_eq!(parsed.pattern, "");
4923 }
4924
4925 #[test]
4926 fn proto3_enum_wire_strings_default_to_empty_when_omitted() {
4927 // A `String` field carrying an enum-shaped wire value is never
4928 // actually empty on a real response, but proto3 still omits it
4929 // whenever it happens to be an empty string, so these need the
4930 // same defensive `#[serde(default)]` as every other scalar here
4931 // (issue #1929's follow-up review finding).
4932 let point: InterpolationPoint =
4933 serde_json::from_value(serde_json::json!({"colorStyle": {"rgbColor": {}}})).unwrap();
4934 assert_eq!(point.point_type, "");
4935
4936 let basic_chart: BasicChartSpec = serde_json::from_value(serde_json::json!({})).unwrap();
4937 assert_eq!(basic_chart.chart_type, "");
4938
4939 let axis: BasicChartAxis = serde_json::from_value(serde_json::json!({})).unwrap();
4940 assert_eq!(axis.position, "");
4941
4942 let pivot_value: PivotValue = serde_json::from_value(serde_json::json!({})).unwrap();
4943 assert_eq!(pivot_value.summarize_function, "");
4944 }
4945
4946 /// Strips proto3's default scalar values (`0`, `0.0`, `false`, `""`)
4947 /// from a JSON value exactly as the real Sheets API omits them from a
4948 /// response, so a round-trip test can simulate what a `Deserialize`
4949 /// struct here actually has to tolerate. Only scalars at any nesting
4950 /// depth are stripped — a `Vec`/array's own elements are left alone,
4951 /// since proto3 does not omit elements of a non-empty repeated field.
4952 fn proto3_omit_zeros(value: serde_json::Value) -> serde_json::Value {
4953 match value {
4954 serde_json::Value::Object(map) => {
4955 let mut out = serde_json::Map::new();
4956 for (key, val) in map {
4957 let val = proto3_omit_zeros(val);
4958 let is_zero_scalar = match &val {
4959 serde_json::Value::Bool(b) => !*b,
4960 serde_json::Value::Number(n) => n.as_f64() == Some(0.0),
4961 serde_json::Value::String(s) => s.is_empty(),
4962 _ => false,
4963 };
4964 if !is_zero_scalar {
4965 out.insert(key, val);
4966 }
4967 }
4968 serde_json::Value::Object(out)
4969 }
4970 serde_json::Value::Array(items) => {
4971 serde_json::Value::Array(items.into_iter().map(proto3_omit_zeros).collect())
4972 }
4973 other => other,
4974 }
4975 }
4976
4977 /// A `Spreadsheet` exercising every struct this issue touched, with
4978 /// every **required** (non-`Option`) scalar field this issue fixed
4979 /// sitting right on its proto3 zero boundary (sheet id 0, row 1,
4980 /// column A, a zero colour channel, `--no-totals`, offset 0, a
4981 /// column-A sort).
4982 ///
4983 /// Deliberately avoids putting a zero value in any `Option`-typed
4984 /// scalar field (e.g. `GridRange::start_row_index`,
4985 /// `SlicerSpec::column_index`): those already collapse `Some(0)` and
4986 /// `None` to the same wire shape independent of this issue (an
4987 /// `Option` defaults to `None` whether the key is absent or its own
4988 /// `#[serde(default)]` fires), so a round-trip through them is
4989 /// ambiguous by construction and would make this guard fail on
4990 /// something it was never meant to catch — see
4991 /// `SlicerSpec::column_index`'s own doc comment and the
4992 /// `update-slicer` carry-forward this issue fixed for that specific,
4993 /// already-known case.
4994 fn zero_boundary_spreadsheet() -> Spreadsheet {
4995 let sheet_id = 0; // the first sheet — an omitted `sheetId` means this
4996 let full_range = GridRange {
4997 sheet_id,
4998 start_row_index: Some(1),
4999 end_row_index: Some(6),
5000 start_column_index: Some(1),
5001 end_column_index: Some(3),
5002 };
5003 Spreadsheet {
5004 spreadsheet_id: Some("zero-boundary".to_string()),
5005 properties: Some(SpreadsheetProperties {
5006 title: "Zero boundaries".to_string(),
5007 ..Default::default()
5008 }),
5009 sheets: vec![Sheet {
5010 properties: Some(SheetProperties {
5011 sheet_id: Some(1), // an unrelated `Option<i64>`; see doc comment
5012 title: "Sheet1".to_string(),
5013 ..Default::default()
5014 }),
5015 basic_filter: Some(BasicFilter {
5016 range: Some(full_range),
5017 sort_specs: vec![SortSpec {
5018 dimension_index: 0, // column A
5019 sort_order: SortOrder::Ascending,
5020 }],
5021 criteria: BTreeMap::new(),
5022 }),
5023 conditional_formats: vec![ConditionalFormatRule {
5024 ranges: vec![full_range],
5025 boolean_rule: Some(BooleanRule {
5026 condition: BooleanCondition {
5027 condition_type: "NUMBER_GREATER".to_string(),
5028 values: vec![ConditionValue {
5029 user_entered_value: Some("0".to_string()),
5030 relative_date: None,
5031 }],
5032 },
5033 format: CellFormat {
5034 background_color_style: Some(ColorStyle::rgb(Color {
5035 red: 0.0,
5036 green: 0.0,
5037 blue: 0.0,
5038 })),
5039 number_format: Some(NumberFormat {
5040 format_type: "NUMBER".to_string(),
5041 pattern: String::new(),
5042 }),
5043 ..Default::default()
5044 },
5045 }),
5046 gradient_rule: None,
5047 }],
5048 charts: vec![EmbeddedChart {
5049 chart_id: Some(1),
5050 spec: Some(ChartSpec {
5051 title: Some("Chart".to_string()),
5052 subtitle: None,
5053 basic_chart: Some(BasicChartSpec {
5054 chart_type: "COLUMN".to_string(),
5055 legend_position: None,
5056 stacked_type: None,
5057 header_count: None,
5058 axis: Vec::new(),
5059 domains: vec![BasicChartDomain {
5060 domain: ChartData {
5061 source_range: ChartSourceRange {
5062 sources: vec![GridRange {
5063 sheet_id,
5064 start_row_index: Some(1),
5065 end_row_index: Some(4),
5066 start_column_index: Some(1),
5067 end_column_index: Some(2),
5068 }],
5069 },
5070 extra: BTreeMap::new(),
5071 },
5072 extra: BTreeMap::new(),
5073 }],
5074 series: vec![BasicChartSeries {
5075 series: ChartData {
5076 source_range: ChartSourceRange {
5077 sources: vec![GridRange {
5078 sheet_id,
5079 start_row_index: Some(1),
5080 end_row_index: Some(4),
5081 start_column_index: Some(2),
5082 end_column_index: Some(3),
5083 }],
5084 },
5085 extra: BTreeMap::new(),
5086 },
5087 target_axis: None,
5088 extra: BTreeMap::new(),
5089 }],
5090 extra: BTreeMap::new(),
5091 }),
5092 pie_chart: None,
5093 extra: BTreeMap::new(),
5094 }),
5095 position: Some(EmbeddedObjectPosition {
5096 sheet_id: None,
5097 overlay_position: Some(OverlayPosition {
5098 // Anchored at A1 on sheet 0 — every field zero.
5099 anchor_cell: GridCoordinate {
5100 sheet_id,
5101 row_index: 0,
5102 column_index: 0,
5103 },
5104 offset_x_pixels: None,
5105 offset_y_pixels: None,
5106 width_pixels: None,
5107 height_pixels: None,
5108 }),
5109 new_sheet: None,
5110 }),
5111 border: None,
5112 }],
5113 slicers: vec![Slicer {
5114 slicer_id: Some(2),
5115 spec: Some(SlicerSpec {
5116 data_range: Some(full_range),
5117 filter_criteria: None,
5118 // Deliberately non-zero — see this function's doc
5119 // comment on `Option`-typed scalar ambiguity.
5120 // Column 0's own read-back is covered separately by
5121 // `embedded_object.rs`'s carry-forward tests.
5122 column_index: Some(2),
5123 apply_to_pivot_tables: Some(true),
5124 title: None,
5125 extra: BTreeMap::new(),
5126 }),
5127 position: Some(EmbeddedObjectPosition {
5128 sheet_id: None,
5129 overlay_position: Some(OverlayPosition {
5130 anchor_cell: GridCoordinate {
5131 sheet_id,
5132 row_index: 10,
5133 column_index: 3,
5134 },
5135 offset_x_pixels: None,
5136 offset_y_pixels: None,
5137 width_pixels: None,
5138 height_pixels: None,
5139 }),
5140 new_sheet: None,
5141 }),
5142 }],
5143 banded_ranges: vec![BandedRange {
5144 banded_range_id: Some(3),
5145 range: Some(full_range),
5146 row_properties: Some(BandingProperties {
5147 header_color_style: None,
5148 first_band_color_style: Some(ColorStyle::rgb(Color {
5149 red: 0.0,
5150 green: 0.0,
5151 blue: 0.0,
5152 })),
5153 second_band_color_style: Some(ColorStyle::rgb(Color {
5154 red: 1.0,
5155 green: 1.0,
5156 blue: 1.0,
5157 })),
5158 footer_color_style: None,
5159 }),
5160 column_properties: None,
5161 }],
5162 row_groups: vec![DimensionGroup {
5163 range: DimensionRange {
5164 sheet_id, // sheet 0
5165 dimension: Dimension::Rows,
5166 start_index: 0, // row 1
5167 end_index: 3,
5168 },
5169 depth: 1,
5170 collapsed: false,
5171 }],
5172 data: vec![GridData {
5173 // Non-zero — see this function's doc comment on
5174 // `Option`-typed scalar ambiguity.
5175 start_row: Some(1),
5176 start_column: Some(1),
5177 row_data: vec![RowData {
5178 values: vec![CellSnapshot {
5179 pivot_table: Some(PivotTable {
5180 source: full_range,
5181 rows: vec![PivotGroup {
5182 source_column_offset: 0, // source's first column
5183 show_totals: false, // --no-totals
5184 sort_order: Some("ASCENDING".to_string()),
5185 }],
5186 columns: Vec::new(),
5187 values: vec![PivotValue {
5188 source_column_offset: 0, // source's first column
5189 summarize_function: "SUM".to_string(),
5190 name: None,
5191 }],
5192 filter_specs: vec![PivotFilterSpec {
5193 column_offset_index: 0, // source's first column
5194 filter_criteria: PivotFilterCriteria {
5195 visible_values: vec!["keep".to_string()],
5196 },
5197 }],
5198 value_layout: None,
5199 }),
5200 formatted_value: None,
5201 ..Default::default()
5202 }],
5203 }],
5204 }],
5205 ..Default::default()
5206 }],
5207 ..Default::default()
5208 }
5209 }
5210
5211 #[test]
5212 fn proto3_omit_zeros_strips_only_zero_valued_scalars() {
5213 let stripped = proto3_omit_zeros(serde_json::json!({
5214 "a": 0,
5215 "b": 0.0,
5216 "c": false,
5217 "d": "",
5218 "e": 1,
5219 "f": "kept",
5220 "g": [0, "", {"h": 0, "i": 1}],
5221 "j": {},
5222 }));
5223 assert_eq!(
5224 stripped,
5225 serde_json::json!({
5226 "e": 1,
5227 "f": "kept",
5228 "g": [0, "", {"i": 1}],
5229 "j": {},
5230 })
5231 );
5232 }
5233
5234 #[test]
5235 fn proto3_omit_zeros_round_trip_recovers_every_zero_boundary_field() {
5236 // Guards against the next scalar field added to this file skipping
5237 // `#[serde(default)]`: simulate the wire dropping every zero-valued
5238 // scalar (exactly what a real Sheets response does), then confirm
5239 // deserializing that stripped shape still recovers the original.
5240 let original = zero_boundary_spreadsheet();
5241 let json = serde_json::to_value(&original).unwrap();
5242 let wire_shape = proto3_omit_zeros(json);
5243 let round_tripped: Spreadsheet = serde_json::from_value(wire_shape)
5244 .expect("every zero-valued scalar here must have #[serde(default)]");
5245 assert_eq!(round_tripped, original);
5246 }
5247}