1use serde::Serialize;
5
6use super::anchor::{self, AnchorError, Side};
7use super::types::{Document, StructuralElement};
8use super::write_types::{
9 DocsRequest, InsertTableColumnRequest, InsertTableRequest, InsertTableRowRequest, Location,
10 TableCellLocation, TableDimensionRequest,
11};
12use crate::drive::write_gate::DriveOperation;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "kebab-case")]
17pub enum TableVerb {
18 InsertTable,
20 InsertRow,
22 InsertColumn,
24 DeleteRow,
26 DeleteColumn,
28}
29
30impl TableVerb {
31 #[must_use]
33 pub const fn label(self) -> &'static str {
34 match self {
35 Self::InsertTable => "insert-table",
36 Self::InsertRow => "insert-table-row",
37 Self::InsertColumn => "insert-table-column",
38 Self::DeleteRow => "delete-table-row",
39 Self::DeleteColumn => "delete-table-column",
40 }
41 }
42
43 #[must_use]
45 pub const fn gate_operation(self) -> DriveOperation {
46 match self {
47 Self::DeleteRow | Self::DeleteColumn => DriveOperation::DocsTableDelete,
48 _ => DriveOperation::DocsStructure,
49 }
50 }
51
52 pub(super) const fn log_operation(self) -> &'static str {
53 match self {
54 Self::InsertTable => "docs-insert-table",
55 Self::InsertRow => "docs-insert-table-row",
56 Self::InsertColumn => "docs-insert-table-column",
57 Self::DeleteRow => "docs-delete-table-row",
58 Self::DeleteColumn => "docs-delete-table-column",
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum TableEdit {
66 Insert {
68 anchor: String,
70 side: Side,
72 rows: i64,
74 columns: i64,
76 match_case: bool,
78 },
79 Dimension {
81 verb: TableVerb,
83 cell: String,
85 after: bool,
87 match_case: bool,
89 },
90}
91
92impl TableEdit {
93 #[must_use]
95 pub const fn verb(&self) -> TableVerb {
96 match self {
97 Self::Insert { .. } => TableVerb::InsertTable,
98 Self::Dimension { verb, .. } => *verb,
99 }
100 }
101
102 pub(super) fn validate(&self) -> Result<(), String> {
103 let anchor = match self {
104 Self::Insert {
105 anchor,
106 rows,
107 columns,
108 ..
109 } => {
110 if *rows < 1
112 || *columns < 1
113 || rows.checked_mul(*columns).is_none_or(|n| n > 10_000)
114 {
115 return Err(
116 "table must have positive dimensions and at most 10000 cells".into(),
117 );
118 }
119 anchor
120 }
121 Self::Dimension {
122 verb: TableVerb::InsertTable,
123 ..
124 } => {
125 return Err("table insertion requires dimensions and a body anchor".into());
126 }
127 Self::Dimension { cell, .. } => cell,
128 };
129 if anchor.is_empty() || anchor.contains(['\n', '\r']) {
130 return Err("anchor must be nonempty and within one paragraph".into());
131 }
132 Ok(())
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138pub struct TablePreview {
139 pub operation: TableVerb,
141 pub location: Location,
143 pub row_index: Option<i64>,
145 pub column_index: Option<i64>,
147 pub rows_before: i64,
149 pub columns_before: i64,
151 pub rows_after: i64,
153 pub columns_after: i64,
155 pub insert_after: bool,
157 pub preceding_newline: bool,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
163#[serde(tag = "reason", rename_all = "kebab-case")]
164pub enum TableError {
165 Anchor {
167 error: AnchorError,
169 },
170 UnsafeBoundary,
172 InvalidTable,
174 SuggestedTable,
176 UnsupportedTable,
178 LastDimension,
180 InvalidIntent,
182}
183
184fn bounds(element: &StructuralElement, index: i64) -> bool {
185 element
186 .start_index
187 .is_some_and(|start| start >= 1 && start <= index)
188 && element.end_index.is_some_and(|end| index < end)
189}
190
191pub fn resolve(
193 document: &Document,
194 edit: &TableEdit,
195) -> Result<(DocsRequest, TablePreview), TableError> {
196 edit.validate().map_err(|_| TableError::InvalidIntent)?;
197 let (needle, side, match_case) = match edit {
198 TableEdit::Insert {
199 anchor,
200 side,
201 match_case,
202 ..
203 } => (anchor, *side, *match_case),
204 TableEdit::Dimension {
205 cell, match_case, ..
206 } => (cell, Side::Before, *match_case),
207 };
208 let point = anchor::resolve_insert(document, needle, side, "x", match_case)
209 .map_err(|error| TableError::Anchor { error })?;
210 let tabs = document.resolved_tabs();
211 let body = tabs
212 .iter()
213 .find(|tab| tab.tab_id == point.tab_id.as_deref())
214 .and_then(|tab| tab.body)
215 .ok_or(TableError::UnsafeBoundary)?;
216 let elements: Vec<_> = body
217 .content
218 .iter()
219 .filter(|e| bounds(e, point.start_index))
220 .collect();
221 if elements.len() != 1 {
222 return Err(TableError::UnsafeBoundary);
223 }
224 let element = elements[0];
225 let verb = edit.verb();
226 let mut preview = TablePreview {
227 operation: verb,
228 location: Location {
229 index: point.start_index,
230 tab_id: point.tab_id,
231 segment_id: None,
232 },
233 row_index: None,
234 column_index: None,
235 rows_before: 0,
236 columns_before: 0,
237 rows_after: 0,
238 columns_after: 0,
239 insert_after: false,
240 preceding_newline: false,
241 };
242 if let TableEdit::Insert { rows, columns, .. } = edit {
243 if element.paragraph.is_none()
244 || element.table.is_some()
245 || element.table_of_contents.is_some()
246 || element.section_break.is_some()
247 {
248 return Err(TableError::UnsafeBoundary);
249 }
250 preview.rows_after = *rows;
251 preview.columns_after = *columns;
252 preview.preceding_newline = true;
253 return Ok((
254 DocsRequest::InsertTable(InsertTableRequest {
255 rows: *rows,
256 columns: *columns,
257 location: preview.location.clone(),
258 }),
259 preview,
260 ));
261 }
262 let table = element.table.as_ref().ok_or(TableError::UnsafeBoundary)?;
263 let json = serde_json::to_value(table).map_err(|_| TableError::InvalidTable)?;
264 if table.has_pending_suggestions || super::types::has_suggestions(&json) {
265 return Err(TableError::SuggestedTable);
266 }
267 let rows = table
268 .rows
269 .filter(|n| *n > 0)
270 .ok_or(TableError::InvalidTable)?;
271 let columns = table
272 .columns
273 .filter(|n| *n > 0)
274 .ok_or(TableError::InvalidTable)?;
275 if usize::try_from(rows).ok() != Some(table.table_rows.len()) {
276 return Err(TableError::InvalidTable);
277 }
278 let start = element.start_index.ok_or(TableError::InvalidTable)?;
279 let end = element.end_index.ok_or(TableError::InvalidTable)?;
280 let mut previous_row_end = start + 1;
281 let mut reference = None;
282 for (r, row) in table.table_rows.iter().enumerate() {
283 if usize::try_from(columns).ok() != Some(row.table_cells.len()) {
284 return Err(TableError::UnsupportedTable);
285 }
286 let rs = row.start_index.ok_or(TableError::InvalidTable)?;
287 let re = row.end_index.ok_or(TableError::InvalidTable)?;
288 if rs < previous_row_end || re <= rs || re > end {
289 return Err(TableError::InvalidTable);
290 }
291 previous_row_end = re;
292 let mut previous_cell_end = rs + 1;
293 for (c, cell) in row.table_cells.iter().enumerate() {
294 let cs = cell.start_index.ok_or(TableError::InvalidTable)?;
295 let ce = cell.end_index.ok_or(TableError::InvalidTable)?;
296 if cs < previous_cell_end || ce <= cs || ce > re {
297 return Err(TableError::InvalidTable);
298 }
299 previous_cell_end = ce;
300 if cell.table_cell_style.as_ref().is_some_and(|style| {
301 style.row_span.is_some_and(|n| n != 1) || style.column_span.is_some_and(|n| n != 1)
302 }) {
303 return Err(TableError::UnsupportedTable);
304 }
305 if cell.content.is_empty()
306 || cell.content.iter().any(|e| {
307 e.paragraph.is_none()
308 || e.table.is_some()
309 || e.table_of_contents.is_some()
310 || e.section_break.is_some()
311 })
312 {
313 return Err(TableError::UnsupportedTable);
314 }
315 let mut previous_paragraph_end = cs + 1;
316 for paragraph in &cell.content {
317 let ps = paragraph.start_index.ok_or(TableError::InvalidTable)?;
318 let pe = paragraph.end_index.ok_or(TableError::InvalidTable)?;
319 if ps != previous_paragraph_end || pe <= ps || pe > ce {
320 return Err(TableError::InvalidTable);
321 }
322 previous_paragraph_end = pe;
323 }
324 if previous_paragraph_end != ce {
325 return Err(TableError::InvalidTable);
326 }
327 if cs <= preview.location.index && preview.location.index < ce {
328 if reference.is_some() {
329 return Err(TableError::InvalidTable); }
331 reference = Some((r as i64, c as i64));
332 }
333 }
334 }
335 let (row, column) = reference.ok_or(TableError::InvalidTable)?;
336 preview.location.index = start;
337 preview.row_index = Some(row);
338 preview.column_index = Some(column);
339 preview.rows_before = rows;
340 preview.columns_before = columns;
341 preview.rows_after = rows;
342 preview.columns_after = columns;
343 let cell = TableCellLocation {
344 table_start_location: preview.location.clone(),
345 row_index: row,
346 column_index: column,
347 };
348 let TableEdit::Dimension { after, .. } = edit else {
349 return Err(TableError::InvalidIntent); };
351 let request = match verb {
352 TableVerb::InsertRow => {
353 preview.rows_after = rows.checked_add(1).ok_or(TableError::InvalidTable)?;
354 preview.insert_after = *after;
355 DocsRequest::InsertTableRow(InsertTableRowRequest {
356 table_cell_location: cell,
357 insert_below: *after,
358 })
359 }
360 TableVerb::InsertColumn => {
361 preview.columns_after = columns.checked_add(1).ok_or(TableError::InvalidTable)?;
362 preview.insert_after = *after;
363 DocsRequest::InsertTableColumn(InsertTableColumnRequest {
364 table_cell_location: cell,
365 insert_right: *after,
366 })
367 }
368 TableVerb::DeleteRow => {
369 if rows == 1 {
370 return Err(TableError::LastDimension);
371 }
372 preview.rows_after -= 1;
373 DocsRequest::DeleteTableRow(TableDimensionRequest {
374 table_cell_location: cell,
375 })
376 }
377 TableVerb::DeleteColumn => {
378 if columns == 1 {
379 return Err(TableError::LastDimension);
380 }
381 preview.columns_after -= 1;
382 DocsRequest::DeleteTableColumn(TableDimensionRequest {
383 table_cell_location: cell,
384 })
385 }
386 TableVerb::InsertTable => return Err(TableError::InvalidIntent), };
388 Ok((request, preview))
389}
390
391#[cfg(test)]
392pub(crate) fn fixture() -> serde_json::Value {
393 use serde_json::json;
394 let mut index = 10;
395 let start = index;
396 index += 1;
397 let mut rows = Vec::new();
398 for names in [["😀 Alpha", "Beta"], ["Gamma", "Delta"]] {
399 let rs = index;
400 index += 1;
401 let mut cells = Vec::new();
402 for name in names {
403 let cs = index;
404 index += 1;
405 let ps = index;
406 let text = format!("{name}\n");
407 index += text.encode_utf16().count() as i64;
408 cells.push(json!({"startIndex":cs,"endIndex":index,"tableCellStyle":{"rowSpan":1,"columnSpan":1},"content":[{"startIndex":ps,"endIndex":index,"paragraph":{"elements":[{"startIndex":ps,"endIndex":index,"textRun":{"content":text}}]}}]}));
409 }
410 rows.push(json!({"startIndex":rs,"endIndex":index,"tableCells":cells}));
411 }
412 json!({"revisionId":"rev-table","tabs":[{"documentTab":{"body":{"content":[
413 {"startIndex":1,"endIndex":10,"paragraph":{"elements":[{"startIndex":1,"endIndex":10,"textRun":{"content":"😀 Intro\n"}}]}},
414 {"startIndex":start,"endIndex":index,"table":{"rows":2,"columns":2,"tableRows":rows}}
415 ]}},"tabProperties":{"tabId":"child"}}]})
416}
417
418#[cfg(test)]
419#[allow(clippy::unwrap_used, clippy::expect_used)]
420mod tests {
421 use super::super::write_types::BatchUpdateDocumentRequest;
422 use super::*;
423 use serde_json::json;
424
425 fn doc(value: serde_json::Value) -> Document {
426 serde_json::from_value(value).unwrap()
427 }
428 fn dimension(verb: TableVerb) -> TableEdit {
429 TableEdit::Dimension {
430 verb,
431 cell: "Alpha".into(),
432 after: true,
433 match_case: true,
434 }
435 }
436 fn table(value: &mut serde_json::Value) -> &mut serde_json::Value {
437 &mut value["tabs"][0]["documentTab"]["body"]["content"][1]["table"]
438 }
439
440 #[test]
441 fn table_insertion_uses_utf16_indices_tab_identity_and_preceding_newline() {
442 for (side, index) in [(Side::Before, 4), (Side::After, 9)] {
443 let edit = TableEdit::Insert {
444 anchor: "Intro".into(),
445 side,
446 rows: 2,
447 columns: 3,
448 match_case: true,
449 };
450 let (request, preview) = resolve(&doc(fixture()), &edit).unwrap();
451 assert_eq!(preview.location.index, index);
452 assert!(preview.preceding_newline);
453 assert_eq!(
454 (
455 preview.rows_before,
456 preview.columns_before,
457 preview.rows_after,
458 preview.columns_after
459 ),
460 (0, 0, 2, 3)
461 );
462 assert_eq!(
463 serde_json::to_value(BatchUpdateDocumentRequest::new(request, "revision")).unwrap(),
464 json!({"requests":[{"insertTable":{"rows":2,"columns":3,"location":{"index":index,"tabId":"child"}}}],"writeControl":{"requiredRevisionId":"revision"}})
465 );
466 }
467 }
468
469 #[test]
470 fn all_dimension_wire_shapes_and_effects_are_exact() {
471 for (verb, key, extra, dimensions) in [
472 (
473 TableVerb::InsertRow,
474 "insertTableRow",
475 Some("insertBelow"),
476 (3, 2),
477 ),
478 (
479 TableVerb::InsertColumn,
480 "insertTableColumn",
481 Some("insertRight"),
482 (2, 3),
483 ),
484 (TableVerb::DeleteRow, "deleteTableRow", None, (1, 2)),
485 (TableVerb::DeleteColumn, "deleteTableColumn", None, (2, 1)),
486 ] {
487 for after in [false, true] {
488 let edit = TableEdit::Dimension {
489 verb,
490 cell: "Alpha".into(),
491 after,
492 match_case: true,
493 };
494 let (request, preview) = resolve(&doc(fixture()), &edit).unwrap();
495 assert_eq!((preview.rows_after, preview.columns_after), dimensions);
496 assert_eq!(
497 (preview.row_index, preview.column_index),
498 (Some(0), Some(0))
499 );
500 let wire =
501 serde_json::to_value(BatchUpdateDocumentRequest::new(request, "rev")).unwrap();
502 let mut expected = json!({"tableCellLocation":{"tableStartLocation":{"index":10,"tabId":"child"},"rowIndex":0,"columnIndex":0}});
503 if let Some(field) = extra {
504 expected[field] = json!(after);
505 }
506 assert_eq!(wire["requests"], json!([{key:expected}]));
507 assert_eq!(wire["writeControl"], json!({"requiredRevisionId":"rev"}));
508 }
509 }
510 }
511
512 #[test]
513 fn refuses_merged_nested_suggested_and_invalid_tables() {
514 for (kind, expected) in [
515 (0, TableError::UnsupportedTable),
516 (1, TableError::UnsupportedTable),
517 (2, TableError::SuggestedTable),
518 (3, TableError::InvalidTable),
519 (4, TableError::SuggestedTable),
520 (5, TableError::UnsupportedTable),
521 ] {
522 let mut value = fixture();
523 let t = table(&mut value);
524 match kind {
525 0 => t["tableRows"][0]["tableCells"][0]["tableCellStyle"]["rowSpan"] = json!(2),
526 1 => t["tableRows"][1]["tableCells"][1]["content"][0]["table"] = json!({}),
527 2 => t["tableRows"][1]["suggestedDeletionIds"] = json!(["suggestion"]),
528 3 => t["tableRows"][0]["tableCells"][0]["endIndex"] = json!(1000),
529 4 => {
530 t["tableRows"][1]["tableCells"][1]["content"][0]["paragraph"]["elements"][0]
531 ["textRun"]["suggestedInsertionIds"] = json!(["s"]);
532 }
533 _ => {
534 t["tableRows"][1]["tableCells"]
535 .as_array_mut()
536 .unwrap()
537 .pop();
538 }
539 }
540 assert_eq!(
541 resolve(&doc(value), &dimension(TableVerb::DeleteRow)).unwrap_err(),
542 expected,
543 "kind {kind}"
544 );
545 }
546 }
547
548 #[test]
549 fn refuses_last_dimension_and_body_cell_boundary_confusion() {
550 let mut value = fixture();
551 let t = table(&mut value);
552 t["rows"] = json!(1);
553 t["tableRows"].as_array_mut().unwrap().pop();
554 assert_eq!(
555 resolve(&doc(value), &dimension(TableVerb::DeleteRow)).unwrap_err(),
556 TableError::LastDimension
557 );
558 let edit = TableEdit::Insert {
559 anchor: "Alpha".into(),
560 side: Side::After,
561 rows: 1,
562 columns: 1,
563 match_case: true,
564 };
565 assert_eq!(
566 resolve(&doc(fixture()), &edit).unwrap_err(),
567 TableError::UnsafeBoundary
568 );
569 let edit = TableEdit::Dimension {
570 verb: TableVerb::InsertRow,
571 cell: "Intro".into(),
572 after: false,
573 match_case: true,
574 };
575 assert_eq!(
576 resolve(&doc(fixture()), &edit).unwrap_err(),
577 TableError::UnsafeBoundary
578 );
579 let mut value = fixture();
580 let t = table(&mut value);
581 t["columns"] = json!(1);
582 for row in t["tableRows"].as_array_mut().unwrap() {
583 row["tableCells"].as_array_mut().unwrap().pop();
584 }
585 assert_eq!(
586 resolve(&doc(value), &dimension(TableVerb::DeleteColumn)).unwrap_err(),
587 TableError::LastDimension
588 );
589 }
590
591 #[test]
592 fn invalid_missing_ambiguous_and_suggested_anchors_fail_closed() {
593 for (needle, expected) in [
594 ("", AnchorError::InvalidAnchor),
595 ("missing", AnchorError::NotFound),
596 ("a", AnchorError::Ambiguous { count: 5 }),
597 ] {
598 let edit = TableEdit::Dimension {
599 verb: TableVerb::InsertRow,
600 cell: needle.into(),
601 after: false,
602 match_case: true,
603 };
604 let error = resolve(&doc(fixture()), &edit).unwrap_err();
605 if needle.is_empty() {
606 assert_eq!(error, TableError::InvalidIntent);
607 } else {
608 assert_eq!(error, TableError::Anchor { error: expected });
609 }
610 }
611 let mut value = fixture();
612 table(&mut value)["tableRows"][0]["tableCells"][0]["content"][0]["paragraph"]["elements"]
613 [0]["textRun"]["suggestedDeletionIds"] = json!(["s"]);
614 assert_eq!(
615 resolve(&doc(value), &dimension(TableVerb::InsertRow)).unwrap_err(),
616 TableError::Anchor {
617 error: AnchorError::SuggestedContent
618 }
619 );
620 }
621
622 fn insert(anchor: &str, rows: i64, columns: i64) -> TableEdit {
623 TableEdit::Insert {
624 anchor: anchor.into(),
625 side: Side::After,
626 rows,
627 columns,
628 match_case: true,
629 }
630 }
631
632 #[test]
635 fn every_verb_has_its_own_label_log_name_and_permission() {
636 for (verb, label, log, gate) in [
637 (
638 TableVerb::InsertTable,
639 "insert-table",
640 "docs-insert-table",
641 DriveOperation::DocsStructure,
642 ),
643 (
644 TableVerb::InsertRow,
645 "insert-table-row",
646 "docs-insert-table-row",
647 DriveOperation::DocsStructure,
648 ),
649 (
650 TableVerb::InsertColumn,
651 "insert-table-column",
652 "docs-insert-table-column",
653 DriveOperation::DocsStructure,
654 ),
655 (
656 TableVerb::DeleteRow,
657 "delete-table-row",
658 "docs-delete-table-row",
659 DriveOperation::DocsTableDelete,
660 ),
661 (
662 TableVerb::DeleteColumn,
663 "delete-table-column",
664 "docs-delete-table-column",
665 DriveOperation::DocsTableDelete,
666 ),
667 ] {
668 assert_eq!(verb.label(), label);
669 assert_eq!(verb.log_operation(), log);
670 assert_eq!(verb.gate_operation(), gate);
671 }
672 }
673
674 #[test]
675 fn an_edit_reports_the_verb_it_describes() {
676 assert_eq!(insert("Intro", 1, 1).verb(), TableVerb::InsertTable);
677 for verb in [TableVerb::InsertRow, TableVerb::DeleteColumn] {
678 assert_eq!(dimension(verb).verb(), verb);
679 }
680 }
681
682 #[test]
685 fn insertion_refuses_non_positive_oversized_and_overflowing_grids() {
686 for (rows, columns) in [
687 (0, 1),
688 (1, 0),
689 (-1, 5),
690 (5, -1),
691 (1, 10_001),
692 (10_001, 1),
693 (101, 100),
694 (i64::MAX, 2),
695 ] {
696 assert_eq!(
697 resolve(&doc(fixture()), &insert("Intro", rows, columns)).unwrap_err(),
698 TableError::InvalidIntent,
699 "{rows}x{columns}"
700 );
701 }
702 for (rows, columns) in [(1, 1), (1, 10_000), (10_000, 1), (100, 100)] {
703 assert!(
704 resolve(&doc(fixture()), &insert("Intro", rows, columns)).is_ok(),
705 "{rows}x{columns}"
706 );
707 }
708 }
709
710 #[test]
711 fn insertion_and_dimension_edits_refuse_an_anchor_that_spans_paragraphs() {
712 for anchor in ["a\nb", "a\rb", ""] {
713 assert_eq!(
714 resolve(&doc(fixture()), &insert(anchor, 1, 1)).unwrap_err(),
715 TableError::InvalidIntent,
716 "{anchor:?}"
717 );
718 let edit = TableEdit::Dimension {
719 verb: TableVerb::DeleteRow,
720 cell: anchor.into(),
721 after: false,
722 match_case: true,
723 };
724 assert_eq!(
725 resolve(&doc(fixture()), &edit).unwrap_err(),
726 TableError::InvalidIntent,
727 "{anchor:?}"
728 );
729 }
730 }
731
732 #[test]
735 fn a_dimension_edit_cannot_insert_a_table() {
736 assert_eq!(
737 resolve(&doc(fixture()), &dimension(TableVerb::InsertTable)).unwrap_err(),
738 TableError::InvalidIntent
739 );
740 }
741
742 #[test]
745 fn an_anchor_inside_overlapping_body_elements_is_an_unsafe_boundary() {
746 let mut value = fixture();
747 value["tabs"][0]["documentTab"]["body"]["content"][1]["startIndex"] = json!(1);
748 assert_eq!(
749 resolve(&doc(value), &insert("Intro", 1, 1)).unwrap_err(),
750 TableError::UnsafeBoundary
751 );
752 }
753
754 #[test]
757 fn inconsistent_table_dimensions_and_index_ranges_fail_closed() {
758 type Corrupt = fn(&mut serde_json::Value);
759 let cases: [(&str, Corrupt); 8] = [
760 ("declared rows disagree with the row list", |value| {
761 table(value)["rows"] = json!(3);
762 }),
763 ("no declared rows", |value| {
764 table(value)["rows"] = json!(0);
765 }),
766 ("no declared columns", |value| {
767 table(value)["columns"] = json!(0);
768 }),
769 ("row overruns the table", |value| {
770 table(value)["tableRows"][0]["endIndex"] = json!(1000);
771 }),
772 ("row has no end", |value| {
773 table(value)["tableRows"][0]
774 .as_object_mut()
775 .unwrap()
776 .remove("endIndex");
777 }),
778 ("cell has no start", |value| {
779 table(value)["tableRows"][1]["tableCells"][1]
780 .as_object_mut()
781 .unwrap()
782 .remove("startIndex");
783 }),
784 (
785 "cell's first paragraph does not follow its start",
786 |value| {
787 table(value)["tableRows"][1]["tableCells"][1]["startIndex"] = json!(37);
788 },
789 ),
790 ("cell's paragraphs stop short of its end", |value| {
791 value["tabs"][0]["documentTab"]["body"]["content"][1]["endIndex"] = json!(44);
792 table(value)["tableRows"][1]["endIndex"] = json!(44);
793 table(value)["tableRows"][1]["tableCells"][1]["endIndex"] = json!(44);
794 }),
795 ];
796 for (name, corrupt) in cases {
797 let mut value = fixture();
798 corrupt(&mut value);
799 assert_eq!(
800 resolve(&doc(value), &dimension(TableVerb::DeleteRow)).unwrap_err(),
801 TableError::InvalidTable,
802 "{name}"
803 );
804 }
805 }
806
807 #[test]
808 fn unmodelled_inline_suggestions_and_bad_cell_paragraph_bounds_fail_closed() {
809 let mut value = fixture();
810 table(&mut value)["tableRows"][1]["tableCells"][1]["content"][0]["paragraph"]
811 ["suggestedParagraphStyleChanges"] = json!({"s": {}});
812 let document = doc(value);
813 assert_eq!(
814 resolve(&document, &dimension(TableVerb::DeleteColumn)).unwrap_err(),
815 TableError::SuggestedTable
816 );
817 let mut value = fixture();
818 table(&mut value)["tableRows"][0]["tableCells"][0]["startIndex"] = json!(1);
819 assert_eq!(
820 resolve(&doc(value), &dimension(TableVerb::DeleteColumn)).unwrap_err(),
821 TableError::InvalidTable
822 );
823 let mut value = fixture();
824 value["tabs"][0]["tabProperties"]["tabId"] = json!("");
825 assert_eq!(
826 resolve(&doc(value), &dimension(TableVerb::InsertRow)).unwrap_err(),
827 TableError::Anchor {
828 error: AnchorError::InvalidIndices
829 }
830 );
831 }
832}