Skip to main content

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}