spreadsheet_mcp/diff/
tables.rs1use anyhow::Result;
2use quick_xml::events::Event;
3use quick_xml::reader::Reader;
4use schemars::JsonSchema;
5use serde::Serialize;
6use std::collections::{HashMap, HashSet};
7use std::io::BufRead;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct TableInfo {
11 pub display_name: String,
12 pub range: String, pub sheet: String, }
15
16#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum TableDiff {
19 TableAdded {
20 display_name: String,
21 sheet: String,
22 range: String,
23 },
24 TableDeleted {
25 display_name: String,
26 sheet: String,
27 },
28 TableModified {
29 display_name: String,
30 sheet: String,
31 old_range: String,
32 new_range: String,
33 },
34}
35
36pub fn parse_table_xml<R: BufRead>(
37 reader: &mut Reader<R>,
38 sheet_name: String,
39) -> Result<TableInfo> {
40 let mut buf = Vec::new();
41 let mut display_name = String::new();
42 let mut range = String::new();
43
44 loop {
45 match reader.read_event_into(&mut buf) {
46 Ok(Event::Start(ref e)) if e.name().as_ref() == b"table" => {
47 for attr in e.attributes() {
48 let attr = attr?;
49 match attr.key.as_ref() {
50 b"displayName" => {
51 display_name = String::from_utf8_lossy(&attr.value).to_string()
52 }
53 b"ref" => range = String::from_utf8_lossy(&attr.value).to_string(),
54 _ => {}
55 }
56 }
57 break;
59 }
60 Ok(Event::Eof) => break,
61 Err(e) => return Err(e.into()),
62 _ => {}
63 }
64 buf.clear();
65 }
66
67 if display_name.is_empty() || range.is_empty() {
68 if display_name.is_empty() {
72 return Err(anyhow::anyhow!("Missing displayName in table definition"));
73 }
74 }
75
76 Ok(TableInfo {
77 display_name,
78 range,
79 sheet: sheet_name,
80 })
81}
82
83pub fn diff_tables(
84 base_tables: &HashMap<String, TableInfo>, fork_tables: &HashMap<String, TableInfo>,
86) -> Vec<TableDiff> {
87 let mut diffs = Vec::new();
88 let all_keys: HashSet<_> = base_tables.keys().chain(fork_tables.keys()).collect();
89
90 for key in all_keys {
91 let base = base_tables.get(key);
92 let fork = fork_tables.get(key);
93
94 match (base, fork) {
95 (None, Some(f)) => {
96 diffs.push(TableDiff::TableAdded {
97 display_name: f.display_name.clone(),
98 sheet: f.sheet.clone(),
99 range: f.range.clone(),
100 });
101 }
102 (Some(b), None) => {
103 diffs.push(TableDiff::TableDeleted {
104 display_name: b.display_name.clone(),
105 sheet: b.sheet.clone(),
106 });
107 }
108 (Some(b), Some(f)) => {
109 if b.range != f.range {
111 diffs.push(TableDiff::TableModified {
112 display_name: b.display_name.clone(),
113 sheet: b.sheet.clone(),
114 old_range: b.range.clone(),
115 new_range: f.range.clone(),
116 });
117 }
118 }
124 (None, None) => unreachable!(),
125 }
126 }
127
128 diffs.sort_by(|a, b| {
130 let name_a = match a {
131 TableDiff::TableAdded { display_name, .. } => display_name,
132 TableDiff::TableDeleted { display_name, .. } => display_name,
133 TableDiff::TableModified { display_name, .. } => display_name,
134 };
135 let name_b = match b {
136 TableDiff::TableAdded { display_name, .. } => display_name,
137 TableDiff::TableDeleted { display_name, .. } => display_name,
138 TableDiff::TableModified { display_name, .. } => display_name,
139 };
140 name_a.cmp(name_b)
141 });
142
143 diffs
144}