spreadsheet_mcp/diff/
names.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, Eq, Hash)]
10pub struct NameKey {
11 pub name: String,
12 pub scope: Option<u32>, }
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct DefinedName {
17 pub key: NameKey,
18 pub formula: String,
19 pub hidden: bool,
20}
21
22#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
23#[serde(tag = "type", rename_all = "snake_case")]
24pub enum NameDiff {
25 NameAdded {
26 name: String,
27 formula: String,
28 scope_sheet: Option<String>,
29 },
30 NameDeleted {
31 name: String,
32 scope_sheet: Option<String>,
33 },
34 NameModified {
35 name: String,
36 scope_sheet: Option<String>,
37 old_formula: String,
38 new_formula: String,
39 },
40}
41
42pub fn parse_defined_names<R: BufRead>(
43 reader: &mut Reader<R>,
44) -> Result<HashMap<NameKey, DefinedName>> {
45 let mut names = HashMap::new();
46 let mut buf = Vec::new();
47 let mut inner_buf = Vec::new();
48
49 loop {
61 match reader.read_event_into(&mut buf) {
62 Ok(Event::Start(ref e)) if e.name().as_ref() == b"definedName" => {
63 let mut name_attr = String::new();
64 let mut scope_attr = None;
65 let mut hidden = false;
66
67 for attr in e.attributes() {
68 let attr = attr?;
69 match attr.key.as_ref() {
70 b"name" => name_attr = String::from_utf8_lossy(&attr.value).to_string(),
71 b"localSheetId" => {
72 if let Ok(val) = String::from_utf8_lossy(&attr.value).parse::<u32>() {
73 scope_attr = Some(val);
74 }
75 }
76 b"hidden" => {
77 let val = attr.value.as_ref();
78 hidden = val == b"1" || val == b"true";
79 }
80 _ => {}
81 }
82 }
83
84 let mut formula = String::new();
86 loop {
87 match reader.read_event_into(&mut inner_buf) {
88 Ok(Event::Text(e)) => formula.push_str(&e.unescape()?),
89 Ok(Event::End(ref end)) if end.name() == e.name() => break,
90 Ok(Event::Eof) => break,
91 Err(e) => return Err(e.into()),
92 _ => {}
93 }
94 inner_buf.clear();
95 }
96
97 let key = NameKey {
98 name: name_attr,
99 scope: scope_attr,
100 };
101
102 names.insert(
103 key.clone(),
104 DefinedName {
105 key,
106 formula,
107 hidden,
108 },
109 );
110 }
111 Ok(Event::End(ref e)) if e.name().as_ref() == b"definedNames" => break,
112 Ok(Event::Eof) => break,
113 Err(e) => return Err(e.into()),
114 _ => {}
115 }
116 buf.clear();
117 }
118
119 Ok(names)
120}
121
122pub fn diff_names(
123 base_names: &HashMap<NameKey, DefinedName>,
124 fork_names: &HashMap<NameKey, DefinedName>,
125 sheet_id_map: &HashMap<u32, String>, ) -> Vec<NameDiff> {
127 let mut diffs = Vec::new();
128 let all_keys: HashSet<_> = base_names.keys().chain(fork_names.keys()).collect();
129
130 for key in all_keys {
131 let base = base_names.get(key);
132 let fork = fork_names.get(key);
133
134 if let Some(b) = base
135 && b.hidden
136 {
137 continue;
138 }
139 if let Some(f) = fork
140 && f.hidden
141 {
142 continue;
143 }
144
145 let sheet_name = key.scope.and_then(|id| sheet_id_map.get(&id).cloned());
146
147 match (base, fork) {
148 (None, Some(f)) => {
149 diffs.push(NameDiff::NameAdded {
150 name: key.name.clone(),
151 formula: f.formula.clone(),
152 scope_sheet: sheet_name,
153 });
154 }
155 (Some(_), None) => {
156 diffs.push(NameDiff::NameDeleted {
157 name: key.name.clone(),
158 scope_sheet: sheet_name,
159 });
160 }
161 (Some(b), Some(f)) => {
162 if b.formula != f.formula {
163 diffs.push(NameDiff::NameModified {
164 name: key.name.clone(),
165 scope_sheet: sheet_name,
166 old_formula: b.formula.clone(),
167 new_formula: f.formula.clone(),
168 });
169 }
170 }
171 (None, None) => unreachable!(),
172 }
173 }
174
175 diffs.sort_by(|a, b| {
177 let name_a = match a {
178 NameDiff::NameAdded { name, .. } => name,
179 NameDiff::NameDeleted { name, .. } => name,
180 NameDiff::NameModified { name, .. } => name,
181 };
182 let name_b = match b {
183 NameDiff::NameAdded { name, .. } => name,
184 NameDiff::NameDeleted { name, .. } => name,
185 NameDiff::NameModified { name, .. } => name,
186 };
187 name_a.cmp(name_b)
188 });
189
190 diffs
191}