1use crate::error::Result;
7use sheetkit_xml::worksheet::{DataValidation, DataValidations, WorksheetXml};
8
9#[derive(Debug, Clone, PartialEq)]
11pub enum ValidationType {
12 Whole,
13 Decimal,
14 List,
15 Date,
16 Time,
17 TextLength,
18 Custom,
19}
20
21impl ValidationType {
22 pub fn as_str(&self) -> &str {
24 match self {
25 ValidationType::Whole => "whole",
26 ValidationType::Decimal => "decimal",
27 ValidationType::List => "list",
28 ValidationType::Date => "date",
29 ValidationType::Time => "time",
30 ValidationType::TextLength => "textLength",
31 ValidationType::Custom => "custom",
32 }
33 }
34
35 pub fn parse(s: &str) -> Option<Self> {
37 match s {
38 "whole" => Some(ValidationType::Whole),
39 "decimal" => Some(ValidationType::Decimal),
40 "list" => Some(ValidationType::List),
41 "date" => Some(ValidationType::Date),
42 "time" => Some(ValidationType::Time),
43 "textLength" => Some(ValidationType::TextLength),
44 "custom" => Some(ValidationType::Custom),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum ValidationOperator {
53 Between,
54 NotBetween,
55 Equal,
56 NotEqual,
57 LessThan,
58 LessThanOrEqual,
59 GreaterThan,
60 GreaterThanOrEqual,
61}
62
63impl ValidationOperator {
64 pub fn as_str(&self) -> &str {
66 match self {
67 ValidationOperator::Between => "between",
68 ValidationOperator::NotBetween => "notBetween",
69 ValidationOperator::Equal => "equal",
70 ValidationOperator::NotEqual => "notEqual",
71 ValidationOperator::LessThan => "lessThan",
72 ValidationOperator::LessThanOrEqual => "lessThanOrEqual",
73 ValidationOperator::GreaterThan => "greaterThan",
74 ValidationOperator::GreaterThanOrEqual => "greaterThanOrEqual",
75 }
76 }
77
78 pub fn parse(s: &str) -> Option<Self> {
80 match s {
81 "between" => Some(ValidationOperator::Between),
82 "notBetween" => Some(ValidationOperator::NotBetween),
83 "equal" => Some(ValidationOperator::Equal),
84 "notEqual" => Some(ValidationOperator::NotEqual),
85 "lessThan" => Some(ValidationOperator::LessThan),
86 "lessThanOrEqual" => Some(ValidationOperator::LessThanOrEqual),
87 "greaterThan" => Some(ValidationOperator::GreaterThan),
88 "greaterThanOrEqual" => Some(ValidationOperator::GreaterThanOrEqual),
89 _ => None,
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub enum ErrorStyle {
97 Stop,
98 Warning,
99 Information,
100}
101
102impl ErrorStyle {
103 pub fn as_str(&self) -> &str {
105 match self {
106 ErrorStyle::Stop => "stop",
107 ErrorStyle::Warning => "warning",
108 ErrorStyle::Information => "information",
109 }
110 }
111
112 pub fn parse(s: &str) -> Option<Self> {
114 match s {
115 "stop" => Some(ErrorStyle::Stop),
116 "warning" => Some(ErrorStyle::Warning),
117 "information" => Some(ErrorStyle::Information),
118 _ => None,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct DataValidationConfig {
126 pub sqref: String,
128 pub validation_type: ValidationType,
130 pub operator: Option<ValidationOperator>,
132 pub formula1: Option<String>,
134 pub formula2: Option<String>,
136 pub allow_blank: bool,
138 pub error_style: Option<ErrorStyle>,
140 pub error_title: Option<String>,
142 pub error_message: Option<String>,
144 pub prompt_title: Option<String>,
146 pub prompt_message: Option<String>,
148 pub show_input_message: bool,
150 pub show_error_message: bool,
152}
153
154impl DataValidationConfig {
155 pub fn dropdown(sqref: &str, items: &[&str]) -> Self {
159 let formula = format!("\"{}\"", items.join(","));
160 Self {
161 sqref: sqref.to_string(),
162 validation_type: ValidationType::List,
163 operator: None,
164 formula1: Some(formula),
165 formula2: None,
166 allow_blank: true,
167 error_style: Some(ErrorStyle::Stop),
168 error_title: None,
169 error_message: None,
170 prompt_title: None,
171 prompt_message: None,
172 show_input_message: true,
173 show_error_message: true,
174 }
175 }
176
177 pub fn whole_number(sqref: &str, min: i64, max: i64) -> Self {
179 Self {
180 sqref: sqref.to_string(),
181 validation_type: ValidationType::Whole,
182 operator: Some(ValidationOperator::Between),
183 formula1: Some(min.to_string()),
184 formula2: Some(max.to_string()),
185 allow_blank: true,
186 error_style: Some(ErrorStyle::Stop),
187 error_title: None,
188 error_message: None,
189 prompt_title: None,
190 prompt_message: None,
191 show_input_message: true,
192 show_error_message: true,
193 }
194 }
195
196 pub fn decimal(sqref: &str, min: f64, max: f64) -> Self {
198 Self {
199 sqref: sqref.to_string(),
200 validation_type: ValidationType::Decimal,
201 operator: Some(ValidationOperator::Between),
202 formula1: Some(min.to_string()),
203 formula2: Some(max.to_string()),
204 allow_blank: true,
205 error_style: Some(ErrorStyle::Stop),
206 error_title: None,
207 error_message: None,
208 prompt_title: None,
209 prompt_message: None,
210 show_input_message: true,
211 show_error_message: true,
212 }
213 }
214
215 pub fn text_length(sqref: &str, operator: ValidationOperator, length: u32) -> Self {
217 Self {
218 sqref: sqref.to_string(),
219 validation_type: ValidationType::TextLength,
220 operator: Some(operator),
221 formula1: Some(length.to_string()),
222 formula2: None,
223 allow_blank: true,
224 error_style: Some(ErrorStyle::Stop),
225 error_title: None,
226 error_message: None,
227 prompt_title: None,
228 prompt_message: None,
229 show_input_message: true,
230 show_error_message: true,
231 }
232 }
233}
234
235pub fn config_to_xml(config: &DataValidationConfig) -> DataValidation {
237 DataValidation {
238 validation_type: Some(config.validation_type.as_str().to_string()),
239 operator: config.operator.as_ref().map(|o| o.as_str().to_string()),
240 allow_blank: if config.allow_blank { Some(true) } else { None },
241 show_input_message: if config.show_input_message {
242 Some(true)
243 } else {
244 None
245 },
246 show_error_message: if config.show_error_message {
247 Some(true)
248 } else {
249 None
250 },
251 error_style: config.error_style.as_ref().map(|e| e.as_str().to_string()),
252 error_title: config.error_title.clone(),
253 error: config.error_message.clone(),
254 prompt_title: config.prompt_title.clone(),
255 prompt: config.prompt_message.clone(),
256 sqref: config.sqref.clone(),
257 formula1: config.formula1.clone(),
258 formula2: config.formula2.clone(),
259 }
260}
261
262fn xml_to_config(dv: &DataValidation) -> DataValidationConfig {
264 DataValidationConfig {
265 sqref: dv.sqref.clone(),
266 validation_type: dv
267 .validation_type
268 .as_deref()
269 .and_then(ValidationType::parse)
270 .unwrap_or(ValidationType::Whole),
271 operator: dv.operator.as_deref().and_then(ValidationOperator::parse),
272 formula1: dv.formula1.clone(),
273 formula2: dv.formula2.clone(),
274 allow_blank: dv.allow_blank.unwrap_or(false),
275 error_style: dv.error_style.as_deref().and_then(ErrorStyle::parse),
276 error_title: dv.error_title.clone(),
277 error_message: dv.error.clone(),
278 prompt_title: dv.prompt_title.clone(),
279 prompt_message: dv.prompt.clone(),
280 show_input_message: dv.show_input_message.unwrap_or(false),
281 show_error_message: dv.show_error_message.unwrap_or(false),
282 }
283}
284
285pub fn add_validation(ws: &mut WorksheetXml, config: &DataValidationConfig) -> Result<()> {
287 let dv = config_to_xml(config);
288 let dvs = ws.data_validations.get_or_insert_with(|| DataValidations {
289 count: Some(0),
290 data_validations: Vec::new(),
291 });
292 dvs.data_validations.push(dv);
293 dvs.count = Some(dvs.data_validations.len() as u32);
294 Ok(())
295}
296
297pub fn get_validations(ws: &WorksheetXml) -> Vec<DataValidationConfig> {
299 match &ws.data_validations {
300 Some(dvs) => dvs.data_validations.iter().map(xml_to_config).collect(),
301 None => Vec::new(),
302 }
303}
304
305pub fn remove_validation(ws: &mut WorksheetXml, sqref: &str) -> Result<()> {
309 if let Some(ref mut dvs) = ws.data_validations {
310 dvs.data_validations.retain(|dv| dv.sqref != sqref);
311 dvs.count = Some(dvs.data_validations.len() as u32);
312 if dvs.data_validations.is_empty() {
313 ws.data_validations = None;
314 }
315 }
316 Ok(())
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_dropdown_validation() {
325 let config = DataValidationConfig::dropdown("A1:A100", &["Yes", "No", "Maybe"]);
326 assert_eq!(config.sqref, "A1:A100");
327 assert_eq!(config.validation_type, ValidationType::List);
328 assert_eq!(config.formula1, Some("\"Yes,No,Maybe\"".to_string()));
329 assert!(config.allow_blank);
330 assert!(config.show_input_message);
331 assert!(config.show_error_message);
332 }
333
334 #[test]
335 fn test_whole_number_validation() {
336 let config = DataValidationConfig::whole_number("B1:B50", 1, 100);
337 assert_eq!(config.sqref, "B1:B50");
338 assert_eq!(config.validation_type, ValidationType::Whole);
339 assert_eq!(config.operator, Some(ValidationOperator::Between));
340 assert_eq!(config.formula1, Some("1".to_string()));
341 assert_eq!(config.formula2, Some("100".to_string()));
342 }
343
344 #[test]
345 fn test_decimal_validation() {
346 let config = DataValidationConfig::decimal("C1:C10", 0.0, 99.99);
347 assert_eq!(config.sqref, "C1:C10");
348 assert_eq!(config.validation_type, ValidationType::Decimal);
349 assert_eq!(config.operator, Some(ValidationOperator::Between));
350 assert_eq!(config.formula1, Some("0".to_string()));
351 assert_eq!(config.formula2, Some("99.99".to_string()));
352 }
353
354 #[test]
355 fn test_text_length_validation() {
356 let config =
357 DataValidationConfig::text_length("D1:D10", ValidationOperator::LessThanOrEqual, 255);
358 assert_eq!(config.sqref, "D1:D10");
359 assert_eq!(config.validation_type, ValidationType::TextLength);
360 assert_eq!(config.operator, Some(ValidationOperator::LessThanOrEqual));
361 assert_eq!(config.formula1, Some("255".to_string()));
362 }
363
364 #[test]
365 fn test_config_to_xml_roundtrip() {
366 let config = DataValidationConfig::dropdown("A1:A10", &["Red", "Blue"]);
367 let xml = config_to_xml(&config);
368 assert_eq!(xml.validation_type, Some("list".to_string()));
369 assert_eq!(xml.sqref, "A1:A10");
370 assert_eq!(xml.formula1, Some("\"Red,Blue\"".to_string()));
371 assert_eq!(xml.allow_blank, Some(true));
372 assert_eq!(xml.show_input_message, Some(true));
373 assert_eq!(xml.show_error_message, Some(true));
374 }
375
376 #[test]
377 fn test_add_validation_to_worksheet() {
378 let mut ws = WorksheetXml::default();
379 let config = DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
380 add_validation(&mut ws, &config).unwrap();
381
382 assert!(ws.data_validations.is_some());
383 let dvs = ws.data_validations.as_ref().unwrap();
384 assert_eq!(dvs.count, Some(1));
385 assert_eq!(dvs.data_validations.len(), 1);
386 assert_eq!(dvs.data_validations[0].sqref, "A1:A100");
387 }
388
389 #[test]
390 fn test_add_multiple_validations() {
391 let mut ws = WorksheetXml::default();
392 let config1 = DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
393 let config2 = DataValidationConfig::whole_number("B1:B100", 1, 100);
394 add_validation(&mut ws, &config1).unwrap();
395 add_validation(&mut ws, &config2).unwrap();
396
397 let dvs = ws.data_validations.as_ref().unwrap();
398 assert_eq!(dvs.count, Some(2));
399 assert_eq!(dvs.data_validations.len(), 2);
400 }
401
402 #[test]
403 fn test_get_validations() {
404 let mut ws = WorksheetXml::default();
405 let config = DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
406 add_validation(&mut ws, &config).unwrap();
407
408 let configs = get_validations(&ws);
409 assert_eq!(configs.len(), 1);
410 assert_eq!(configs[0].sqref, "A1:A100");
411 assert_eq!(configs[0].validation_type, ValidationType::List);
412 }
413
414 #[test]
415 fn test_get_validations_empty() {
416 let ws = WorksheetXml::default();
417 let configs = get_validations(&ws);
418 assert!(configs.is_empty());
419 }
420
421 #[test]
422 fn test_remove_validation() {
423 let mut ws = WorksheetXml::default();
424 let config1 = DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
425 let config2 = DataValidationConfig::whole_number("B1:B100", 1, 100);
426 add_validation(&mut ws, &config1).unwrap();
427 add_validation(&mut ws, &config2).unwrap();
428
429 remove_validation(&mut ws, "A1:A100").unwrap();
430
431 let dvs = ws.data_validations.as_ref().unwrap();
432 assert_eq!(dvs.count, Some(1));
433 assert_eq!(dvs.data_validations.len(), 1);
434 assert_eq!(dvs.data_validations[0].sqref, "B1:B100");
435 }
436
437 #[test]
438 fn test_remove_last_validation_clears_container() {
439 let mut ws = WorksheetXml::default();
440 let config = DataValidationConfig::dropdown("A1:A100", &["Yes", "No"]);
441 add_validation(&mut ws, &config).unwrap();
442 remove_validation(&mut ws, "A1:A100").unwrap();
443
444 assert!(ws.data_validations.is_none());
445 }
446
447 #[test]
448 fn test_remove_nonexistent_validation() {
449 let mut ws = WorksheetXml::default();
450 remove_validation(&mut ws, "Z1:Z99").unwrap();
452 assert!(ws.data_validations.is_none());
453 }
454
455 #[test]
456 fn test_validation_xml_serialization_roundtrip() {
457 let mut ws = WorksheetXml::default();
458 let config = DataValidationConfig::dropdown("A1:A10", &["Apple", "Banana"]);
459 add_validation(&mut ws, &config).unwrap();
460
461 let xml = quick_xml::se::to_string(&ws).unwrap();
462 assert!(xml.contains("dataValidations"));
463 assert!(xml.contains("A1:A10"));
464
465 let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
466 assert!(parsed.data_validations.is_some());
467 let dvs = parsed.data_validations.as_ref().unwrap();
468 assert_eq!(dvs.data_validations.len(), 1);
469 assert_eq!(dvs.data_validations[0].sqref, "A1:A10");
470 assert_eq!(
471 dvs.data_validations[0].validation_type,
472 Some("list".to_string())
473 );
474 }
475
476 #[test]
477 fn test_whole_number_validation_xml_roundtrip() {
478 let mut ws = WorksheetXml::default();
479 let config = DataValidationConfig::whole_number("B1:B50", 10, 200);
480 add_validation(&mut ws, &config).unwrap();
481
482 let xml = quick_xml::se::to_string(&ws).unwrap();
483 let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
484
485 let configs = get_validations(&parsed);
486 assert_eq!(configs.len(), 1);
487 assert_eq!(configs[0].sqref, "B1:B50");
488 assert_eq!(configs[0].validation_type, ValidationType::Whole);
489 assert_eq!(configs[0].operator, Some(ValidationOperator::Between));
490 assert_eq!(configs[0].formula1, Some("10".to_string()));
491 assert_eq!(configs[0].formula2, Some("200".to_string()));
492 }
493
494 #[test]
495 fn test_decimal_validation_xml_roundtrip() {
496 let mut ws = WorksheetXml::default();
497 let config = DataValidationConfig::decimal("C1:C10", 1.5, 99.9);
498 add_validation(&mut ws, &config).unwrap();
499
500 let xml = quick_xml::se::to_string(&ws).unwrap();
501 let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
502
503 let configs = get_validations(&parsed);
504 assert_eq!(configs.len(), 1);
505 assert_eq!(configs[0].validation_type, ValidationType::Decimal);
506 }
507
508 #[test]
509 fn test_validation_type_as_str() {
510 assert_eq!(ValidationType::Whole.as_str(), "whole");
511 assert_eq!(ValidationType::Decimal.as_str(), "decimal");
512 assert_eq!(ValidationType::List.as_str(), "list");
513 assert_eq!(ValidationType::Date.as_str(), "date");
514 assert_eq!(ValidationType::Time.as_str(), "time");
515 assert_eq!(ValidationType::TextLength.as_str(), "textLength");
516 assert_eq!(ValidationType::Custom.as_str(), "custom");
517 }
518
519 #[test]
520 fn test_validation_operator_as_str() {
521 assert_eq!(ValidationOperator::Between.as_str(), "between");
522 assert_eq!(ValidationOperator::NotBetween.as_str(), "notBetween");
523 assert_eq!(ValidationOperator::Equal.as_str(), "equal");
524 assert_eq!(ValidationOperator::NotEqual.as_str(), "notEqual");
525 assert_eq!(ValidationOperator::LessThan.as_str(), "lessThan");
526 assert_eq!(
527 ValidationOperator::LessThanOrEqual.as_str(),
528 "lessThanOrEqual"
529 );
530 assert_eq!(ValidationOperator::GreaterThan.as_str(), "greaterThan");
531 assert_eq!(
532 ValidationOperator::GreaterThanOrEqual.as_str(),
533 "greaterThanOrEqual"
534 );
535 }
536
537 #[test]
538 fn test_error_style_as_str() {
539 assert_eq!(ErrorStyle::Stop.as_str(), "stop");
540 assert_eq!(ErrorStyle::Warning.as_str(), "warning");
541 assert_eq!(ErrorStyle::Information.as_str(), "information");
542 }
543}