1use schemars::JsonSchema;
2use serde::de;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6fn normalize_literal(value: &str) -> String {
7 value
8 .chars()
9 .filter(|ch| ch.is_ascii_alphanumeric())
10 .flat_map(|ch| ch.to_lowercase())
11 .collect()
12}
13
14fn levenshtein_distance(left: &str, right: &str) -> usize {
15 if left.is_empty() {
16 return right.chars().count();
17 }
18 if right.is_empty() {
19 return left.chars().count();
20 }
21
22 let right_chars: Vec<char> = right.chars().collect();
23 let mut previous: Vec<usize> = (0..=right_chars.len()).collect();
24 let mut current = vec![0; right_chars.len() + 1];
25
26 for (i, left_ch) in left.chars().enumerate() {
27 current[0] = i + 1;
28 for (j, right_ch) in right_chars.iter().enumerate() {
29 let substitution_cost = if left_ch == *right_ch { 0 } else { 1 };
30 current[j + 1] = (previous[j + 1] + 1)
31 .min(current[j] + 1)
32 .min(previous[j] + substitution_cost);
33 }
34 std::mem::swap(&mut previous, &mut current);
35 }
36
37 previous[right_chars.len()]
38}
39
40fn suggest_literal<'a>(input: &str, valid: &'a [&'a str]) -> Option<&'a str> {
41 let normalized_input = normalize_literal(input);
42 let mut best: Option<(&str, usize)> = None;
43
44 for candidate in valid {
45 let distance = levenshtein_distance(&normalized_input, &normalize_literal(candidate));
46 match best {
47 Some((_, best_distance)) if distance >= best_distance => {}
48 _ => best = Some((candidate, distance)),
49 }
50 }
51
52 match best {
53 Some((candidate, distance)) if distance <= 6 => Some(candidate),
54 _ => None,
55 }
56}
57
58fn enum_value_error(label: &str, input: &str, valid: &[&str], suggestion: Option<&str>) -> String {
59 let valid_list = valid.join("|");
60 match suggestion {
61 Some(candidate) if !candidate.eq_ignore_ascii_case(input) => {
62 format!("invalid {label} '{input}'. Did you mean '{candidate}'? valid: {valid_list}")
63 }
64 _ => format!("invalid {label} '{input}'. valid: {valid_list}"),
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
69#[serde(rename_all = "snake_case")]
70#[derive(Default)]
71pub enum BatchMode {
72 #[default]
73 Apply,
74 Preview,
75}
76
77impl BatchMode {
78 pub fn as_str(self) -> &'static str {
79 match self {
80 Self::Apply => "apply",
81 Self::Preview => "preview",
82 }
83 }
84
85 pub fn is_preview(self) -> bool {
86 matches!(self, Self::Preview)
87 }
88}
89
90impl fmt::Display for BatchMode {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_str(self.as_str())
93 }
94}
95
96impl<'de> Deserialize<'de> for BatchMode {
97 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98 where
99 D: de::Deserializer<'de>,
100 {
101 let s = String::deserialize(deserializer)?;
102 match s.to_ascii_lowercase().as_str() {
103 "apply" => Ok(Self::Apply),
104 "preview" => Ok(Self::Preview),
105 other => {
106 let valid = ["apply", "preview"];
107 let message =
108 enum_value_error("batch_mode", other, &valid, suggest_literal(other, &valid));
109 Err(de::Error::custom(message))
110 }
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
116#[serde(rename_all = "snake_case")]
117#[derive(Default)]
118pub enum ReplaceMatchMode {
119 #[default]
120 Exact,
121 Contains,
122}
123
124impl ReplaceMatchMode {
125 pub fn as_str(self) -> &'static str {
126 match self {
127 Self::Exact => "exact",
128 Self::Contains => "contains",
129 }
130 }
131}
132
133impl<'de> Deserialize<'de> for ReplaceMatchMode {
134 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135 where
136 D: de::Deserializer<'de>,
137 {
138 let s = String::deserialize(deserializer)?;
139 match s.to_ascii_lowercase().as_str() {
140 "exact" => Ok(Self::Exact),
141 "contains" => Ok(Self::Contains),
142 other => {
143 let valid = ["exact", "contains"];
144 let message =
145 enum_value_error("match_mode", other, &valid, suggest_literal(other, &valid));
146 Err(de::Error::custom(message))
147 }
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
153#[serde(rename_all = "snake_case")]
154#[derive(Default)]
155pub enum FillDirection {
156 Down,
157 Right,
158 #[default]
159 Both,
160}
161
162impl FillDirection {
163 pub fn as_str(self) -> &'static str {
164 match self {
165 Self::Down => "down",
166 Self::Right => "right",
167 Self::Both => "both",
168 }
169 }
170}
171
172impl<'de> Deserialize<'de> for FillDirection {
173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174 where
175 D: de::Deserializer<'de>,
176 {
177 let s = String::deserialize(deserializer)?;
178 match s.to_ascii_lowercase().as_str() {
179 "down" => Ok(Self::Down),
180 "right" => Ok(Self::Right),
181 "both" => Ok(Self::Both),
182 other => {
183 let valid = ["down", "right", "both"];
184 let message = enum_value_error(
185 "fill_direction",
186 other,
187 &valid,
188 suggest_literal(other, &valid),
189 );
190 Err(de::Error::custom(message))
191 }
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
197#[serde(rename_all = "snake_case")]
198#[derive(Default)]
199pub enum FormulaRelativeMode {
200 #[default]
201 Excel,
202 AbsCols,
203 AbsRows,
204}
205
206impl<'de> Deserialize<'de> for FormulaRelativeMode {
207 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208 where
209 D: de::Deserializer<'de>,
210 {
211 let s = String::deserialize(deserializer)?;
212 match s.to_ascii_lowercase().as_str() {
213 "excel" => Ok(Self::Excel),
214 "abs_cols" | "abscols" | "columns_absolute" => Ok(Self::AbsCols),
215 "abs_rows" | "absrows" | "rows_absolute" => Ok(Self::AbsRows),
216 other => {
217 let valid = ["excel", "abs_cols", "abs_rows"];
218 let direct_suggestion = match other {
219 "fully_relative" | "fullyrelative" => Some("excel"),
220 _ => None,
221 };
222 let message = enum_value_error(
223 "relative_mode",
224 other,
225 &valid,
226 direct_suggestion.or_else(|| suggest_literal(other, &valid)),
227 );
228 Err(de::Error::custom(message))
229 }
230 }
231 }
232}
233
234impl From<FormulaRelativeMode> for crate::formula::pattern::RelativeMode {
235 fn from(value: FormulaRelativeMode) -> Self {
236 match value {
237 FormulaRelativeMode::Excel => Self::Excel,
238 FormulaRelativeMode::AbsCols => Self::AbsCols,
239 FormulaRelativeMode::AbsRows => Self::AbsRows,
240 }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
245#[serde(rename_all = "snake_case")]
246pub enum PageOrientation {
247 Portrait,
248 Landscape,
249}
250
251impl<'de> Deserialize<'de> for PageOrientation {
252 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253 where
254 D: de::Deserializer<'de>,
255 {
256 let s = String::deserialize(deserializer)?;
257 match s.to_ascii_lowercase().as_str() {
258 "portrait" => Ok(Self::Portrait),
259 "landscape" => Ok(Self::Landscape),
260 other => {
261 let valid = ["portrait", "landscape"];
262 let message = enum_value_error(
263 "page_orientation",
264 other,
265 &valid,
266 suggest_literal(other, &valid),
267 );
268 Err(de::Error::custom(message))
269 }
270 }
271 }
272}
273
274impl PageOrientation {
275 pub fn to_umya(self) -> umya_spreadsheet::OrientationValues {
276 match self {
277 Self::Portrait => umya_spreadsheet::OrientationValues::Portrait,
278 Self::Landscape => umya_spreadsheet::OrientationValues::Landscape,
279 }
280 }
281}