Skip to main content

spreadsheet_mcp/diff/
names.rs

1use 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>, // localSheetId
13}
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    // The reader should be positioned at the start of <definedNames> or we assume the caller
50    // is iterating through workbook.xml.
51    // For simplicity in integration, we'll scan for definedNames block if not inside one,
52    // or just assume we are scanning a stream that might contain it.
53
54    // However, to keep it stateless relative to the outer loop, let's assume the caller
55    // passes a reader that is iterating the *entire* workbook.xml, and we just hook into the relevant events.
56    // OR better: The caller iterates workbook.xml, finds <definedNames>, and calls us to consume it.
57
58    // Let's implement assuming we consume the *content* of <definedNames>.
59
60    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                // Formula is the text content
85                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>, // index -> sheet name
126) -> 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    // Sort for stability
176    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}