1use crate::properties::{PropertyId, PropertyValue};
14use fop_types::{FopError, Result};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct ValidationError {
20 pub property: PropertyId,
22 pub message: String,
24 pub suggestion: Option<String>,
26}
27
28impl ValidationError {
29 pub fn new(property: PropertyId, message: String, suggestion: Option<String>) -> Self {
31 Self {
32 property,
33 message,
34 suggestion,
35 }
36 }
37
38 pub fn to_fop_error(&self, value: &PropertyValue) -> FopError {
40 let mut reason = self.message.clone();
41 if let Some(ref suggestion) = self.suggestion {
42 reason.push_str(" Suggestion: ");
43 reason.push_str(suggestion);
44 }
45 FopError::PropertyValidation {
46 property: self.property.name().to_string(),
47 value: format!("{:?}", value),
48 reason,
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum ValidationRule {
56 Range { min: f64, max: f64 },
58 Enum { allowed: Vec<u16> },
60 StringEnum { allowed: Vec<&'static str> },
62 Required,
64 Conditional {
66 when: PropertyId,
67 has: PropertyValue,
68 },
69}
70
71pub struct PropertyValidator {
73 rules: HashMap<PropertyId, Vec<ValidationRule>>,
74}
75
76impl Default for PropertyValidator {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl PropertyValidator {
83 pub fn new() -> Self {
85 let mut validator = Self {
86 rules: HashMap::new(),
87 };
88 validator.register_default_rules();
89 validator
90 }
91
92 fn register_default_rules(&mut self) {
94 self.add_rule(
96 PropertyId::Opacity,
97 ValidationRule::Range { min: 0.0, max: 1.0 },
98 );
99
100 self.add_rule(
102 PropertyId::ColumnCount,
103 ValidationRule::Range {
104 min: 1.0,
105 max: 255.0,
106 },
107 );
108
109 self.add_rule(
111 PropertyId::Orphans,
112 ValidationRule::Range {
113 min: 1.0,
114 max: 999.0,
115 },
116 );
117
118 self.add_rule(
120 PropertyId::Widows,
121 ValidationRule::Range {
122 min: 1.0,
123 max: 999.0,
124 },
125 );
126
127 self.add_rule(
129 PropertyId::ZIndex,
130 ValidationRule::Range {
131 min: -999999.0,
132 max: 999999.0,
133 },
134 );
135
136 self.add_rule(
138 PropertyId::TextAlign,
139 ValidationRule::StringEnum {
140 allowed: vec!["start", "end", "center", "justify", "left", "right"],
141 },
142 );
143
144 self.add_rule(
146 PropertyId::Overflow,
147 ValidationRule::StringEnum {
148 allowed: vec!["visible", "hidden", "scroll", "auto"],
149 },
150 );
151
152 let border_style_enum = ValidationRule::StringEnum {
154 allowed: vec![
155 "none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset",
156 ],
157 };
158 self.add_rule(PropertyId::BorderStyle, border_style_enum.clone());
159 self.add_rule(PropertyId::BorderTopStyle, border_style_enum.clone());
160 self.add_rule(PropertyId::BorderRightStyle, border_style_enum.clone());
161 self.add_rule(PropertyId::BorderBottomStyle, border_style_enum.clone());
162 self.add_rule(PropertyId::BorderLeftStyle, border_style_enum.clone());
163 self.add_rule(PropertyId::BorderBeforeStyle, border_style_enum.clone());
164 self.add_rule(PropertyId::BorderAfterStyle, border_style_enum.clone());
165 self.add_rule(PropertyId::BorderStartStyle, border_style_enum.clone());
166 self.add_rule(PropertyId::BorderEndStyle, border_style_enum);
167
168 let break_enum = ValidationRule::StringEnum {
170 allowed: vec!["auto", "column", "page", "even-page", "odd-page"],
171 };
172 self.add_rule(PropertyId::BreakBefore, break_enum.clone());
173 self.add_rule(PropertyId::BreakAfter, break_enum);
174 }
175
176 pub fn add_rule(&mut self, property: PropertyId, rule: ValidationRule) {
178 self.rules.entry(property).or_default().push(rule);
179 }
180
181 pub fn validate(&self, id: PropertyId, value: &PropertyValue) -> Result<()> {
183 if matches!(
185 value,
186 PropertyValue::Auto | PropertyValue::Inherit | PropertyValue::None
187 ) {
188 return Ok(());
189 }
190
191 if let Some(rules) = self.rules.get(&id) {
193 for rule in rules {
194 self.validate_rule(id, value, rule)?;
195 }
196 }
197
198 match id {
200 PropertyId::FontSize => Self::validate_positive_length(id, value),
201 PropertyId::LineHeight => Self::validate_line_height(value),
202 _ => Ok(()),
203 }
204 }
205
206 pub fn validate_static(id: PropertyId, value: &PropertyValue) -> Result<()> {
212 if matches!(
214 value,
215 PropertyValue::Auto | PropertyValue::Inherit | PropertyValue::None
216 ) {
217 return Ok(());
218 }
219
220 match id {
223 PropertyId::FontSize => Self::validate_positive_length(id, value),
224 PropertyId::ColumnCount => Self::validate_positive_integer(id, value, 1, 255),
225 PropertyId::Widows => Self::validate_positive_integer(id, value, 1, 999),
226 PropertyId::Orphans => Self::validate_positive_integer(id, value, 1, 999),
227 PropertyId::ZIndex => Self::validate_integer_range(id, value, -999999, 999999),
228 PropertyId::LineHeight => Self::validate_line_height(value),
229 PropertyId::Opacity => Self::validate_range(id, value, 0.0, 1.0),
230 _ => Ok(()),
231 }
232 }
233
234 fn validate_rule(
236 &self,
237 id: PropertyId,
238 value: &PropertyValue,
239 rule: &ValidationRule,
240 ) -> Result<()> {
241 match rule {
242 ValidationRule::Range { min, max } => Self::validate_range(id, value, *min, *max),
243 ValidationRule::Enum { allowed } => Self::validate_enum_values(id, value, allowed),
244 ValidationRule::StringEnum { allowed } => {
245 Self::validate_string_enum(id, value, allowed)
246 }
247 ValidationRule::Required => {
248 Ok(())
250 }
251 ValidationRule::Conditional { .. } => {
252 Ok(())
254 }
255 }
256 }
257
258 fn validate_range(id: PropertyId, value: &PropertyValue, min: f64, max: f64) -> Result<()> {
260 let num = match value {
261 PropertyValue::Number(n) => *n,
262 PropertyValue::Integer(i) => *i as f64,
263 PropertyValue::Length(len) => len.to_pt(),
264 PropertyValue::Percentage(p) => p.as_fraction(),
265 _ => {
266 return Err(FopError::PropertyValidation {
267 property: id.name().to_string(),
268 value: format!("{:?}", value),
269 reason: "Expected numeric value for range validation".to_string(),
270 });
271 }
272 };
273
274 if num < min || num > max {
275 let error = ValidationError::new(
276 id,
277 format!("Value {} is out of range (must be {}-{})", num, min, max),
278 Some(Self::get_range_suggestion(id, min, max)),
279 );
280 return Err(error.to_fop_error(value));
281 }
282
283 Ok(())
284 }
285
286 fn get_range_suggestion(id: PropertyId, min: f64, max: f64) -> String {
288 match id {
289 PropertyId::Opacity => {
290 "Use a value between 0.0 (fully transparent) and 1.0 (fully opaque)".to_string()
291 }
292 PropertyId::ColumnCount => {
293 format!("Column count must be between {} and {} columns", min, max)
294 }
295 PropertyId::Orphans => format!(
296 "Orphans must be between {} and {} lines at the bottom of a page",
297 min, max
298 ),
299 PropertyId::Widows => format!(
300 "Widows must be between {} and {} lines at the top of a page",
301 min, max
302 ),
303 PropertyId::ZIndex => {
304 format!("Z-index must be between {} and {}", min, max)
305 }
306 _ => format!("Value must be between {} and {}", min, max),
307 }
308 }
309
310 fn validate_enum_values(id: PropertyId, value: &PropertyValue, allowed: &[u16]) -> Result<()> {
312 match value {
313 PropertyValue::Enum(e) => {
314 if !allowed.contains(e) {
315 return Err(FopError::PropertyValidation {
316 property: id.name().to_string(),
317 value: format!("{}", e),
318 reason: format!("Allowed enum values: {:?}", allowed),
319 });
320 }
321 Ok(())
322 }
323 _ => Err(FopError::PropertyValidation {
324 property: id.name().to_string(),
325 value: format!("{:?}", value),
326 reason: "Expected enum value".to_string(),
327 }),
328 }
329 }
330
331 fn validate_string_enum(
333 id: PropertyId,
334 value: &PropertyValue,
335 allowed: &[&'static str],
336 ) -> Result<()> {
337 match value {
338 PropertyValue::String(s) => {
339 if !allowed.contains(&s.as_ref()) {
340 let error = ValidationError::new(
341 id,
342 format!("'{}' is not a valid value", s),
343 Some(format!("Use one of: {}", allowed.join(", "))),
344 );
345 return Err(error.to_fop_error(value));
346 }
347 Ok(())
348 }
349 _ => Err(FopError::PropertyValidation {
350 property: id.name().to_string(),
351 value: format!("{:?}", value),
352 reason: format!("Expected one of: {}", allowed.join(", ")),
353 }),
354 }
355 }
356
357 pub fn check_mutual_exclusion(
359 &self,
360 props: &[(PropertyId, PropertyValue)],
361 ) -> Vec<ValidationError> {
362 let mut errors = Vec::new();
363
364 let has_width = props.iter().any(|(id, _)| *id == PropertyId::Width);
366 let has_ipd = props
367 .iter()
368 .any(|(id, _)| *id == PropertyId::InlineProgressionDimension);
369 if has_width && has_ipd {
370 errors.push(ValidationError::new(
371 PropertyId::Width,
372 "Both 'width' and 'inline-progression-dimension' are set".to_string(),
373 Some(
374 "Use 'width' for simple cases or 'inline-progression-dimension' for advanced control, but not both"
375 .to_string(),
376 ),
377 ));
378 }
379
380 let has_height = props.iter().any(|(id, _)| *id == PropertyId::Height);
382 let has_bpd = props
383 .iter()
384 .any(|(id, _)| *id == PropertyId::BlockProgressionDimension);
385 if has_height && has_bpd {
386 errors.push(ValidationError::new(
387 PropertyId::Height,
388 "Both 'height' and 'block-progression-dimension' are set".to_string(),
389 Some(
390 "Use 'height' for simple cases or 'block-progression-dimension' for advanced control, but not both"
391 .to_string(),
392 ),
393 ));
394 }
395
396 errors
397 }
398
399 pub fn validate_length_range(
401 id: PropertyId,
402 value: &PropertyValue,
403 min: f64,
404 max: f64,
405 ) -> Result<()> {
406 match value {
407 PropertyValue::Length(len) => {
408 let pts = len.to_pt();
409 if pts < min || pts > max {
410 let error = ValidationError::new(
411 id,
412 format!("Length value {} is out of range ({}-{})", pts, min, max),
413 Some(format!("Value must be between {}pt and {}pt", min, max)),
414 );
415 return Err(error.to_fop_error(value));
416 }
417 Ok(())
418 }
419 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
420 _ => Err(FopError::PropertyValidation {
421 property: id.name().to_string(),
422 value: format!("{:?}", value),
423 reason: "Expected length value".to_string(),
424 }),
425 }
426 }
427
428 pub fn validate_number_range(
430 id: PropertyId,
431 value: &PropertyValue,
432 min: f64,
433 max: f64,
434 ) -> Result<()> {
435 match value {
436 PropertyValue::Number(n) => {
437 if *n < min || *n > max {
438 let error = ValidationError::new(
439 id,
440 format!("Number value {} is out of range ({}-{})", n, min, max),
441 Some(format!("Value must be between {} and {}", min, max)),
442 );
443 return Err(error.to_fop_error(value));
444 }
445 Ok(())
446 }
447 PropertyValue::Integer(i) => {
448 let n = *i as f64;
449 if n < min || n > max {
450 let error = ValidationError::new(
451 id,
452 format!("Integer value {} is out of range ({}-{})", i, min, max),
453 Some(format!("Value must be between {} and {}", min, max)),
454 );
455 return Err(error.to_fop_error(value));
456 }
457 Ok(())
458 }
459 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
460 _ => Err(FopError::PropertyValidation {
461 property: id.name().to_string(),
462 value: format!("{:?}", value),
463 reason: "Expected numeric value".to_string(),
464 }),
465 }
466 }
467
468 pub fn validate_percentage_range(
470 id: PropertyId,
471 value: &PropertyValue,
472 min: f32,
473 max: f32,
474 ) -> Result<()> {
475 match value {
476 PropertyValue::Percentage(p) => {
477 let fraction = p.as_fraction() as f32;
478 if fraction < min || fraction > max {
479 let error = ValidationError::new(
480 id,
481 format!(
482 "Percentage value {}% is out of range ({}%-{}%)",
483 fraction * 100.0,
484 min * 100.0,
485 max * 100.0
486 ),
487 Some(format!(
488 "Value must be between {}% and {}%",
489 min * 100.0,
490 max * 100.0
491 )),
492 );
493 return Err(error.to_fop_error(value));
494 }
495 Ok(())
496 }
497 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
498 _ => Err(FopError::PropertyValidation {
499 property: id.name().to_string(),
500 value: format!("{:?}", value),
501 reason: "Expected percentage value".to_string(),
502 }),
503 }
504 }
505
506 pub fn validate_enum(
508 id: PropertyId,
509 value: &PropertyValue,
510 allowed_values: &[u16],
511 ) -> Result<()> {
512 Self::validate_enum_values(id, value, allowed_values)
513 }
514
515 fn validate_positive_length(id: PropertyId, value: &PropertyValue) -> Result<()> {
517 match value {
518 PropertyValue::Length(len) => {
519 if len.to_pt() <= 0.0 {
520 let error = ValidationError::new(
521 id,
522 format!("Length value {} must be positive", len.to_pt()),
523 Some("Font size must be greater than 0".to_string()),
524 );
525 return Err(error.to_fop_error(value));
526 }
527 Ok(())
528 }
529 PropertyValue::RelativeFontSize(_) => Ok(()),
531 PropertyValue::Percentage(_) => Ok(()),
533 PropertyValue::Expression(_) => Ok(()),
535 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
536 _ => Err(FopError::PropertyValidation {
537 property: id.name().to_string(),
538 value: format!("{:?}", value),
539 reason: "Expected length value".to_string(),
540 }),
541 }
542 }
543
544 fn validate_positive_integer(
546 id: PropertyId,
547 value: &PropertyValue,
548 min: i32,
549 max: i32,
550 ) -> Result<()> {
551 match value {
552 PropertyValue::Integer(i) => {
553 if *i < min || *i > max {
554 let error = ValidationError::new(
555 id,
556 format!("Integer value {} is out of range ({}-{})", i, min, max),
557 Some(Self::get_range_suggestion(id, min as f64, max as f64)),
558 );
559 return Err(error.to_fop_error(value));
560 }
561 Ok(())
562 }
563 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
564 _ => Err(FopError::PropertyValidation {
565 property: id.name().to_string(),
566 value: format!("{:?}", value),
567 reason: "Expected integer value".to_string(),
568 }),
569 }
570 }
571
572 fn validate_integer_range(
574 id: PropertyId,
575 value: &PropertyValue,
576 min: i32,
577 max: i32,
578 ) -> Result<()> {
579 match value {
580 PropertyValue::Integer(i) => {
581 if *i < min || *i > max {
582 let error = ValidationError::new(
583 id,
584 format!("Integer value {} is out of range ({}-{})", i, min, max),
585 Some(Self::get_range_suggestion(id, min as f64, max as f64)),
586 );
587 return Err(error.to_fop_error(value));
588 }
589 Ok(())
590 }
591 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
592 _ => Err(FopError::PropertyValidation {
593 property: id.name().to_string(),
594 value: format!("{:?}", value),
595 reason: "Expected integer value".to_string(),
596 }),
597 }
598 }
599
600 fn validate_line_height(value: &PropertyValue) -> Result<()> {
602 match value {
603 PropertyValue::Length(len) => {
604 if len.to_pt() <= 0.0 {
605 let error = ValidationError::new(
606 PropertyId::LineHeight,
607 format!("Line height {} must be positive", len.to_pt()),
608 Some("Use a positive length, positive number, or 'normal'".to_string()),
609 );
610 return Err(error.to_fop_error(value));
611 }
612 Ok(())
613 }
614 PropertyValue::Number(n) => {
615 if *n <= 0.0 {
616 let error = ValidationError::new(
617 PropertyId::LineHeight,
618 format!("Line height multiplier {} must be positive", n),
619 Some("Use a positive number (e.g., 1.5 for 150% of font size)".to_string()),
620 );
621 return Err(error.to_fop_error(value));
622 }
623 Ok(())
624 }
625 PropertyValue::String(s) if s.as_ref() == "normal" => Ok(()),
626 PropertyValue::Percentage(p) => {
627 if p.as_fraction() <= 0.0 {
628 let error = ValidationError::new(
629 PropertyId::LineHeight,
630 format!("Line height percentage {} must be positive", p),
631 Some("Use a positive percentage (e.g., 150%)".to_string()),
632 );
633 return Err(error.to_fop_error(value));
634 }
635 Ok(())
636 }
637 PropertyValue::Auto | PropertyValue::None | PropertyValue::Inherit => Ok(()),
638 _ => {
639 let error = ValidationError::new(
640 PropertyId::LineHeight,
641 "Invalid line-height value".to_string(),
642 Some("Use a positive length, positive number, or 'normal'".to_string()),
643 );
644 Err(error.to_fop_error(value))
645 }
646 }
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use fop_types::{Length, Percentage};
654
655 #[test]
656 fn test_opacity_range_validation() {
657 let validator = PropertyValidator::new();
658
659 let valid = PropertyValue::Number(0.5);
661 assert!(validator.validate(PropertyId::Opacity, &valid).is_ok());
662
663 let invalid_high = PropertyValue::Number(1.5);
665 assert!(validator
666 .validate(PropertyId::Opacity, &invalid_high)
667 .is_err());
668
669 let invalid_low = PropertyValue::Number(-0.1);
671 assert!(validator
672 .validate(PropertyId::Opacity, &invalid_low)
673 .is_err());
674
675 let zero = PropertyValue::Number(0.0);
677 assert!(validator.validate(PropertyId::Opacity, &zero).is_ok());
678
679 let one = PropertyValue::Number(1.0);
680 assert!(validator.validate(PropertyId::Opacity, &one).is_ok());
681 }
682
683 #[test]
684 fn test_column_count_range_validation() {
685 let validator = PropertyValidator::new();
686
687 let valid = PropertyValue::Integer(3);
689 assert!(validator.validate(PropertyId::ColumnCount, &valid).is_ok());
690
691 let zero = PropertyValue::Integer(0);
693 assert!(validator.validate(PropertyId::ColumnCount, &zero).is_err());
694
695 let negative = PropertyValue::Integer(-1);
697 assert!(validator
698 .validate(PropertyId::ColumnCount, &negative)
699 .is_err());
700
701 let too_high = PropertyValue::Integer(300);
703 assert!(validator
704 .validate(PropertyId::ColumnCount, &too_high)
705 .is_err());
706
707 let min = PropertyValue::Integer(1);
709 assert!(validator.validate(PropertyId::ColumnCount, &min).is_ok());
710
711 let max = PropertyValue::Integer(255);
712 assert!(validator.validate(PropertyId::ColumnCount, &max).is_ok());
713 }
714
715 #[test]
716 fn test_orphans_widows_validation() {
717 let validator = PropertyValidator::new();
718
719 let valid = PropertyValue::Integer(2);
721 assert!(validator.validate(PropertyId::Orphans, &valid).is_ok());
722 assert!(validator.validate(PropertyId::Widows, &valid).is_ok());
723
724 let zero = PropertyValue::Integer(0);
726 assert!(validator.validate(PropertyId::Orphans, &zero).is_err());
727 assert!(validator.validate(PropertyId::Widows, &zero).is_err());
728
729 let too_high = PropertyValue::Integer(1000);
731 assert!(validator.validate(PropertyId::Orphans, &too_high).is_err());
732 assert!(validator.validate(PropertyId::Widows, &too_high).is_err());
733
734 let min = PropertyValue::Integer(1);
736 assert!(validator.validate(PropertyId::Orphans, &min).is_ok());
737 assert!(validator.validate(PropertyId::Widows, &min).is_ok());
738
739 let max = PropertyValue::Integer(999);
740 assert!(validator.validate(PropertyId::Orphans, &max).is_ok());
741 assert!(validator.validate(PropertyId::Widows, &max).is_ok());
742 }
743
744 #[test]
745 fn test_z_index_validation() {
746 let validator = PropertyValidator::new();
747
748 let positive = PropertyValue::Integer(100);
750 assert!(validator.validate(PropertyId::ZIndex, &positive).is_ok());
751
752 let negative = PropertyValue::Integer(-50);
753 assert!(validator.validate(PropertyId::ZIndex, &negative).is_ok());
754
755 let zero = PropertyValue::Integer(0);
756 assert!(validator.validate(PropertyId::ZIndex, &zero).is_ok());
757
758 let too_high = PropertyValue::Integer(1_000_000);
760 assert!(validator.validate(PropertyId::ZIndex, &too_high).is_err());
761
762 let too_low = PropertyValue::Integer(-1_000_000);
763 assert!(validator.validate(PropertyId::ZIndex, &too_low).is_err());
764
765 let min = PropertyValue::Integer(-999999);
767 assert!(validator.validate(PropertyId::ZIndex, &min).is_ok());
768
769 let max = PropertyValue::Integer(999999);
770 assert!(validator.validate(PropertyId::ZIndex, &max).is_ok());
771 }
772
773 #[test]
774 fn test_text_align_enum_validation() {
775 let validator = PropertyValidator::new();
776
777 let valid_values = vec!["start", "end", "center", "justify", "left", "right"];
779 for value_str in valid_values {
780 let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
781 assert!(validator.validate(PropertyId::TextAlign, &value).is_ok());
782 }
783
784 let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("invalid"));
786 assert!(validator.validate(PropertyId::TextAlign, &invalid).is_err());
787 }
788
789 #[test]
790 fn test_overflow_enum_validation() {
791 let validator = PropertyValidator::new();
792
793 let valid_values = vec!["visible", "hidden", "scroll", "auto"];
795 for value_str in valid_values {
796 let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
797 assert!(validator.validate(PropertyId::Overflow, &value).is_ok());
798 }
799
800 let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("clip"));
802 assert!(validator.validate(PropertyId::Overflow, &invalid).is_err());
803 }
804
805 #[test]
806 fn test_border_style_enum_validation() {
807 let validator = PropertyValidator::new();
808
809 let valid_values = vec![
811 "none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset",
812 ];
813 for value_str in valid_values {
814 let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
815 assert!(validator.validate(PropertyId::BorderStyle, &value).is_ok());
816 assert!(validator
817 .validate(PropertyId::BorderTopStyle, &value)
818 .is_ok());
819 assert!(validator
820 .validate(PropertyId::BorderBottomStyle, &value)
821 .is_ok());
822 }
823
824 let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("wavy"));
826 assert!(validator
827 .validate(PropertyId::BorderStyle, &invalid)
828 .is_err());
829 }
830
831 #[test]
832 fn test_break_enum_validation() {
833 let validator = PropertyValidator::new();
834
835 let valid_values = vec!["auto", "column", "page", "even-page", "odd-page"];
837 for value_str in valid_values {
838 let value = PropertyValue::String(std::borrow::Cow::Borrowed(value_str));
839 assert!(validator.validate(PropertyId::BreakBefore, &value).is_ok());
840 assert!(validator.validate(PropertyId::BreakAfter, &value).is_ok());
841 }
842
843 let invalid = PropertyValue::String(std::borrow::Cow::Borrowed("always"));
845 assert!(validator
846 .validate(PropertyId::BreakBefore, &invalid)
847 .is_err());
848 }
849
850 #[test]
851 fn test_font_size_positive_validation() {
852 let validator = PropertyValidator::new();
853
854 let valid = PropertyValue::Length(Length::from_pt(12.0));
856 assert!(validator.validate(PropertyId::FontSize, &valid).is_ok());
857
858 let invalid = PropertyValue::Length(Length::from_pt(-5.0));
860 assert!(validator.validate(PropertyId::FontSize, &invalid).is_err());
861
862 let zero = PropertyValue::Length(Length::from_pt(0.0));
864 assert!(validator.validate(PropertyId::FontSize, &zero).is_err());
865 }
866
867 #[test]
868 fn test_line_height_validation() {
869 let validator = PropertyValidator::new();
870
871 let valid_length = PropertyValue::Length(Length::from_pt(14.0));
873 assert!(validator
874 .validate(PropertyId::LineHeight, &valid_length)
875 .is_ok());
876
877 let valid_number = PropertyValue::Number(1.5);
879 assert!(validator
880 .validate(PropertyId::LineHeight, &valid_number)
881 .is_ok());
882
883 let normal = PropertyValue::String(std::borrow::Cow::Borrowed("normal"));
885 assert!(validator.validate(PropertyId::LineHeight, &normal).is_ok());
886
887 let invalid_length = PropertyValue::Length(Length::from_pt(0.0));
889 assert!(validator
890 .validate(PropertyId::LineHeight, &invalid_length)
891 .is_err());
892
893 let invalid_number = PropertyValue::Number(-1.0);
895 assert!(validator
896 .validate(PropertyId::LineHeight, &invalid_number)
897 .is_err());
898 }
899
900 #[test]
901 fn test_auto_inherit_none_always_valid() {
902 let validator = PropertyValidator::new();
903
904 assert!(validator
906 .validate(PropertyId::Opacity, &PropertyValue::Auto)
907 .is_ok());
908 assert!(validator
909 .validate(PropertyId::Opacity, &PropertyValue::Inherit)
910 .is_ok());
911 assert!(validator
912 .validate(PropertyId::Opacity, &PropertyValue::None)
913 .is_ok());
914
915 assert!(validator
916 .validate(PropertyId::ColumnCount, &PropertyValue::Auto)
917 .is_ok());
918 assert!(validator
919 .validate(PropertyId::FontSize, &PropertyValue::Inherit)
920 .is_ok());
921 }
922
923 #[test]
924 fn test_mutual_exclusion_width_ipd() {
925 let validator = PropertyValidator::new();
926
927 let props = vec![
928 (
929 PropertyId::Width,
930 PropertyValue::Length(Length::from_pt(100.0)),
931 ),
932 (
933 PropertyId::InlineProgressionDimension,
934 PropertyValue::Length(Length::from_pt(100.0)),
935 ),
936 ];
937
938 let errors = validator.check_mutual_exclusion(&props);
939 assert_eq!(errors.len(), 1);
940 assert_eq!(errors[0].property, PropertyId::Width);
941 }
942
943 #[test]
944 fn test_mutual_exclusion_height_bpd() {
945 let validator = PropertyValidator::new();
946
947 let props = vec![
948 (
949 PropertyId::Height,
950 PropertyValue::Length(Length::from_pt(200.0)),
951 ),
952 (
953 PropertyId::BlockProgressionDimension,
954 PropertyValue::Length(Length::from_pt(200.0)),
955 ),
956 ];
957
958 let errors = validator.check_mutual_exclusion(&props);
959 assert_eq!(errors.len(), 1);
960 assert_eq!(errors[0].property, PropertyId::Height);
961 }
962
963 #[test]
964 fn test_no_mutual_exclusion() {
965 let validator = PropertyValidator::new();
966
967 let props = vec![
968 (
969 PropertyId::Width,
970 PropertyValue::Length(Length::from_pt(100.0)),
971 ),
972 (
973 PropertyId::Height,
974 PropertyValue::Length(Length::from_pt(200.0)),
975 ),
976 ];
977
978 let errors = validator.check_mutual_exclusion(&props);
979 assert_eq!(errors.len(), 0);
980 }
981
982 #[test]
983 fn test_validation_error_to_fop_error() {
984 let error = ValidationError::new(
985 PropertyId::Opacity,
986 "Value out of range".to_string(),
987 Some("Use 0.0-1.0".to_string()),
988 );
989
990 let value = PropertyValue::Number(1.5);
991 let fop_error = error.to_fop_error(&value);
992
993 match fop_error {
994 FopError::PropertyValidation {
995 property,
996 value: _,
997 reason,
998 } => {
999 assert_eq!(property, "opacity");
1000 assert!(reason.contains("Value out of range"));
1001 assert!(reason.contains("Use 0.0-1.0"));
1002 }
1003 _ => panic!("Expected PropertyValidation error"),
1004 }
1005 }
1006
1007 #[test]
1008 fn test_length_range_validation() {
1009 let value = PropertyValue::Length(Length::from_pt(50.0));
1010 assert!(
1011 PropertyValidator::validate_length_range(PropertyId::FontSize, &value, 0.0, 100.0)
1012 .is_ok()
1013 );
1014
1015 assert!(PropertyValidator::validate_length_range(
1016 PropertyId::FontSize,
1017 &value,
1018 60.0,
1019 100.0
1020 )
1021 .is_err());
1022 }
1023
1024 #[test]
1025 fn test_number_range_validation() {
1026 let value = PropertyValue::Number(0.5);
1027 assert!(
1028 PropertyValidator::validate_number_range(PropertyId::Opacity, &value, 0.0, 1.0).is_ok()
1029 );
1030
1031 assert!(
1032 PropertyValidator::validate_number_range(PropertyId::Opacity, &value, 0.6, 1.0)
1033 .is_err()
1034 );
1035 }
1036
1037 #[test]
1038 fn test_percentage_range_validation() {
1039 let value = PropertyValue::Percentage(Percentage::new(0.5)); assert!(
1041 PropertyValidator::validate_percentage_range(PropertyId::Width, &value, 0.0, 1.0)
1042 .is_ok()
1043 );
1044
1045 assert!(
1046 PropertyValidator::validate_percentage_range(PropertyId::Width, &value, 0.6, 1.0)
1047 .is_err()
1048 );
1049 }
1050
1051 #[test]
1052 fn test_enum_validation_legacy() {
1053 let value = PropertyValue::Enum(1);
1054 let allowed = vec![1, 2, 3];
1055 assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &value, &allowed).is_ok());
1056
1057 let invalid = PropertyValue::Enum(5);
1058 assert!(
1059 PropertyValidator::validate_enum(PropertyId::FontStyle, &invalid, &allowed).is_err()
1060 );
1061 }
1062
1063 #[test]
1064 fn test_validator_instance_vs_static() {
1065 let validator = PropertyValidator::new();
1067
1068 let valid_opacity = PropertyValue::Number(0.5);
1069 assert!(validator
1070 .validate(PropertyId::Opacity, &valid_opacity)
1071 .is_ok());
1072 assert!(PropertyValidator::validate_static(PropertyId::Opacity, &valid_opacity).is_ok());
1073
1074 let invalid_opacity = PropertyValue::Number(1.5);
1075 assert!(validator
1076 .validate(PropertyId::Opacity, &invalid_opacity)
1077 .is_err());
1078 assert!(PropertyValidator::validate_static(PropertyId::Opacity, &invalid_opacity).is_err());
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests_extended {
1084 use super::*;
1085 use fop_types::{Length, Percentage};
1086
1087 #[test]
1092 fn test_length_range_exactly_at_min() {
1093 let v = PropertyValue::Length(Length::from_pt(0.0));
1094 assert!(
1095 PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_ok()
1096 );
1097 }
1098
1099 #[test]
1100 fn test_length_range_exactly_at_max() {
1101 let v = PropertyValue::Length(Length::from_pt(100.0));
1102 assert!(
1103 PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_ok()
1104 );
1105 }
1106
1107 #[test]
1108 fn test_length_range_below_min() {
1109 let v = PropertyValue::Length(Length::from_pt(-1.0));
1110 assert!(
1111 PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1112 );
1113 }
1114
1115 #[test]
1116 fn test_length_range_above_max() {
1117 let v = PropertyValue::Length(Length::from_pt(101.0));
1118 assert!(
1119 PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1120 );
1121 }
1122
1123 #[test]
1124 fn test_length_range_auto_passes() {
1125 assert!(PropertyValidator::validate_length_range(
1126 PropertyId::Width,
1127 &PropertyValue::Auto,
1128 0.0,
1129 100.0
1130 )
1131 .is_ok());
1132 }
1133
1134 #[test]
1135 fn test_length_range_inherit_passes() {
1136 assert!(PropertyValidator::validate_length_range(
1137 PropertyId::Width,
1138 &PropertyValue::Inherit,
1139 0.0,
1140 100.0
1141 )
1142 .is_ok());
1143 }
1144
1145 #[test]
1146 fn test_length_range_none_passes() {
1147 assert!(PropertyValidator::validate_length_range(
1148 PropertyId::Width,
1149 &PropertyValue::None,
1150 0.0,
1151 100.0
1152 )
1153 .is_ok());
1154 }
1155
1156 #[test]
1157 fn test_length_range_rejects_non_length() {
1158 let v = PropertyValue::Number(50.0);
1159 assert!(
1160 PropertyValidator::validate_length_range(PropertyId::Width, &v, 0.0, 100.0).is_err()
1161 );
1162 }
1163
1164 #[test]
1169 fn test_number_range_exactly_at_boundaries() {
1170 assert!(PropertyValidator::validate_number_range(
1171 PropertyId::Opacity,
1172 &PropertyValue::Number(0.0),
1173 0.0,
1174 1.0
1175 )
1176 .is_ok());
1177 assert!(PropertyValidator::validate_number_range(
1178 PropertyId::Opacity,
1179 &PropertyValue::Number(1.0),
1180 0.0,
1181 1.0
1182 )
1183 .is_ok());
1184 }
1185
1186 #[test]
1187 fn test_number_range_outside_boundaries() {
1188 assert!(PropertyValidator::validate_number_range(
1189 PropertyId::Opacity,
1190 &PropertyValue::Number(-0.001),
1191 0.0,
1192 1.0
1193 )
1194 .is_err());
1195 assert!(PropertyValidator::validate_number_range(
1196 PropertyId::Opacity,
1197 &PropertyValue::Number(1.001),
1198 0.0,
1199 1.0
1200 )
1201 .is_err());
1202 }
1203
1204 #[test]
1205 fn test_number_range_integer_value() {
1206 let v = PropertyValue::Integer(1);
1208 assert!(
1209 PropertyValidator::validate_number_range(PropertyId::Opacity, &v, 0.0, 2.0).is_ok()
1210 );
1211 }
1212
1213 #[test]
1214 fn test_number_range_auto_passes() {
1215 assert!(PropertyValidator::validate_number_range(
1216 PropertyId::Opacity,
1217 &PropertyValue::Auto,
1218 0.0,
1219 1.0
1220 )
1221 .is_ok());
1222 }
1223
1224 #[test]
1225 fn test_number_range_rejects_string() {
1226 let v = PropertyValue::String(std::borrow::Cow::Borrowed("0.5"));
1227 assert!(
1228 PropertyValidator::validate_number_range(PropertyId::Opacity, &v, 0.0, 1.0).is_err()
1229 );
1230 }
1231
1232 #[test]
1237 fn test_percentage_range_valid() {
1238 let v = PropertyValue::Percentage(Percentage::new(0.5)); assert!(
1240 PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_ok()
1241 );
1242 }
1243
1244 #[test]
1245 fn test_percentage_range_too_high() {
1246 let v = PropertyValue::Percentage(Percentage::new(1.5)); assert!(
1248 PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_err()
1249 );
1250 }
1251
1252 #[test]
1253 fn test_percentage_range_too_low() {
1254 let v = PropertyValue::Percentage(Percentage::new(-0.1));
1255 assert!(
1256 PropertyValidator::validate_percentage_range(PropertyId::Width, &v, 0.0, 1.0).is_err()
1257 );
1258 }
1259
1260 #[test]
1261 fn test_percentage_range_auto_passes() {
1262 assert!(PropertyValidator::validate_percentage_range(
1263 PropertyId::Width,
1264 &PropertyValue::Auto,
1265 0.0,
1266 1.0
1267 )
1268 .is_ok());
1269 }
1270
1271 #[test]
1276 fn test_enum_valid_value() {
1277 let v = PropertyValue::Enum(2);
1278 assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_ok());
1279 }
1280
1281 #[test]
1282 fn test_enum_invalid_value() {
1283 let v = PropertyValue::Enum(99);
1284 assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_err());
1285 }
1286
1287 #[test]
1288 fn test_enum_wrong_type_rejected() {
1289 let v = PropertyValue::String(std::borrow::Cow::Borrowed("normal"));
1290 assert!(PropertyValidator::validate_enum(PropertyId::FontStyle, &v, &[1, 2, 3]).is_err());
1291 }
1292
1293 #[test]
1298 fn test_font_size_mm_valid() {
1299 let v = PropertyValue::Length(Length::from_mm(5.0));
1300 let validator = PropertyValidator::new();
1301 assert!(validator.validate(PropertyId::FontSize, &v).is_ok());
1302 }
1303
1304 #[test]
1305 fn test_font_size_negative_rejected() {
1306 let v = PropertyValue::Length(Length::from_pt(-1.0));
1307 assert!(PropertyValidator::validate_static(PropertyId::FontSize, &v).is_err());
1308 }
1309
1310 #[test]
1311 fn test_font_size_zero_rejected() {
1312 let v = PropertyValue::Length(Length::from_pt(0.0));
1313 assert!(PropertyValidator::validate_static(PropertyId::FontSize, &v).is_err());
1314 }
1315
1316 #[test]
1317 fn test_font_size_percentage_passes() {
1318 let v = PropertyValue::Percentage(Percentage::new(1.2)); let validator = PropertyValidator::new();
1320 assert!(validator.validate(PropertyId::FontSize, &v).is_ok());
1321 }
1322
1323 #[test]
1324 fn test_font_size_inherit_passes() {
1325 let validator = PropertyValidator::new();
1326 assert!(validator
1327 .validate(PropertyId::FontSize, &PropertyValue::Inherit)
1328 .is_ok());
1329 }
1330
1331 #[test]
1336 fn test_line_height_zero_length_rejected() {
1337 let v = PropertyValue::Length(Length::from_pt(0.0));
1338 let validator = PropertyValidator::new();
1339 assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1340 }
1341
1342 #[test]
1343 fn test_line_height_negative_length_rejected() {
1344 let v = PropertyValue::Length(Length::from_pt(-5.0));
1345 let validator = PropertyValidator::new();
1346 assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1347 }
1348
1349 #[test]
1350 fn test_line_height_zero_number_rejected() {
1351 let v = PropertyValue::Number(0.0);
1352 let validator = PropertyValidator::new();
1353 assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1354 }
1355
1356 #[test]
1357 fn test_line_height_negative_number_rejected() {
1358 let v = PropertyValue::Number(-1.5);
1359 let validator = PropertyValidator::new();
1360 assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1361 }
1362
1363 #[test]
1364 fn test_line_height_percentage_valid() {
1365 let v = PropertyValue::Percentage(Percentage::new(1.5)); let validator = PropertyValidator::new();
1367 assert!(validator.validate(PropertyId::LineHeight, &v).is_ok());
1368 }
1369
1370 #[test]
1371 fn test_line_height_invalid_string() {
1372 let v = PropertyValue::String(std::borrow::Cow::Borrowed("tight"));
1373 let validator = PropertyValidator::new();
1374 assert!(validator.validate(PropertyId::LineHeight, &v).is_err());
1375 }
1376
1377 #[test]
1382 fn test_static_opacity_edge_values() {
1383 assert!(PropertyValidator::validate_static(
1384 PropertyId::Opacity,
1385 &PropertyValue::Number(0.0)
1386 )
1387 .is_ok());
1388 assert!(PropertyValidator::validate_static(
1389 PropertyId::Opacity,
1390 &PropertyValue::Number(1.0)
1391 )
1392 .is_ok());
1393 }
1394
1395 #[test]
1396 fn test_static_opacity_out_of_range() {
1397 assert!(PropertyValidator::validate_static(
1398 PropertyId::Opacity,
1399 &PropertyValue::Number(1.01)
1400 )
1401 .is_err());
1402 assert!(PropertyValidator::validate_static(
1403 PropertyId::Opacity,
1404 &PropertyValue::Number(-0.01)
1405 )
1406 .is_err());
1407 }
1408
1409 #[test]
1414 fn test_static_column_count_boundaries() {
1415 assert!(PropertyValidator::validate_static(
1416 PropertyId::ColumnCount,
1417 &PropertyValue::Integer(1)
1418 )
1419 .is_ok());
1420 assert!(PropertyValidator::validate_static(
1421 PropertyId::ColumnCount,
1422 &PropertyValue::Integer(255)
1423 )
1424 .is_ok());
1425 assert!(PropertyValidator::validate_static(
1426 PropertyId::ColumnCount,
1427 &PropertyValue::Integer(0)
1428 )
1429 .is_err());
1430 assert!(PropertyValidator::validate_static(
1431 PropertyId::ColumnCount,
1432 &PropertyValue::Integer(256)
1433 )
1434 .is_err());
1435 }
1436
1437 #[test]
1442 fn test_static_widows_orphans_boundaries() {
1443 assert!(
1444 PropertyValidator::validate_static(PropertyId::Widows, &PropertyValue::Integer(1))
1445 .is_ok()
1446 );
1447 assert!(PropertyValidator::validate_static(
1448 PropertyId::Widows,
1449 &PropertyValue::Integer(999)
1450 )
1451 .is_ok());
1452 assert!(
1453 PropertyValidator::validate_static(PropertyId::Widows, &PropertyValue::Integer(0))
1454 .is_err()
1455 );
1456 assert!(PropertyValidator::validate_static(
1457 PropertyId::Orphans,
1458 &PropertyValue::Integer(1000)
1459 )
1460 .is_err());
1461 }
1462
1463 #[test]
1468 fn test_static_z_index_boundaries() {
1469 assert!(PropertyValidator::validate_static(
1470 PropertyId::ZIndex,
1471 &PropertyValue::Integer(-999999)
1472 )
1473 .is_ok());
1474 assert!(PropertyValidator::validate_static(
1475 PropertyId::ZIndex,
1476 &PropertyValue::Integer(999999)
1477 )
1478 .is_ok());
1479 assert!(PropertyValidator::validate_static(
1480 PropertyId::ZIndex,
1481 &PropertyValue::Integer(1_000_000)
1482 )
1483 .is_err());
1484 assert!(PropertyValidator::validate_static(
1485 PropertyId::ZIndex,
1486 &PropertyValue::Integer(-1_000_000)
1487 )
1488 .is_err());
1489 }
1490
1491 #[test]
1496 fn test_validation_error_no_suggestion() {
1497 let err = ValidationError::new(PropertyId::Opacity, "Bad value".to_string(), None);
1498 let v = PropertyValue::Number(99.0);
1499 let fop_err = err.to_fop_error(&v);
1500 match fop_err {
1501 FopError::PropertyValidation { reason, .. } => {
1502 assert!(reason.contains("Bad value"));
1503 assert!(!reason.contains("Suggestion:"));
1504 }
1505 _ => panic!("Expected PropertyValidation error"),
1506 }
1507 }
1508
1509 #[test]
1510 fn test_validation_error_with_suggestion() {
1511 let err = ValidationError::new(
1512 PropertyId::Opacity,
1513 "Out of range".to_string(),
1514 Some("Use 0.0 to 1.0".to_string()),
1515 );
1516 let v = PropertyValue::Number(99.0);
1517 let fop_err = err.to_fop_error(&v);
1518 match fop_err {
1519 FopError::PropertyValidation { reason, .. } => {
1520 assert!(reason.contains("Out of range"));
1521 assert!(reason.contains("Use 0.0 to 1.0"));
1522 }
1523 _ => panic!("Expected PropertyValidation error"),
1524 }
1525 }
1526
1527 #[test]
1528 fn test_validation_error_equality() {
1529 let e1 = ValidationError::new(PropertyId::Opacity, "msg".to_string(), None);
1530 let e2 = ValidationError::new(PropertyId::Opacity, "msg".to_string(), None);
1531 let e3 = ValidationError::new(PropertyId::Width, "msg".to_string(), None);
1532 assert_eq!(e1, e2);
1533 assert_ne!(e1, e3);
1534 }
1535
1536 #[test]
1541 fn test_validation_rule_range_clone() {
1542 let rule = ValidationRule::Range { min: 0.0, max: 1.0 };
1543 let cloned = rule.clone();
1544 assert_eq!(rule, cloned);
1545 }
1546
1547 #[test]
1548 fn test_validation_rule_string_enum_clone() {
1549 let rule = ValidationRule::StringEnum {
1550 allowed: vec!["a", "b"],
1551 };
1552 let cloned = rule.clone();
1553 assert_eq!(rule, cloned);
1554 }
1555
1556 #[test]
1561 fn test_custom_rule_overrides_for_arbitrary_property() {
1562 let mut validator = PropertyValidator::new();
1563 validator.add_rule(
1565 PropertyId::Width,
1566 ValidationRule::Range {
1567 min: 1.0,
1568 max: 500.0,
1569 },
1570 );
1571 assert!(validator
1572 .validate(
1573 PropertyId::Width,
1574 &PropertyValue::Length(Length::from_pt(100.0))
1575 )
1576 .is_ok());
1577 assert!(validator
1578 .validate(
1579 PropertyId::Width,
1580 &PropertyValue::Length(Length::from_pt(0.0))
1581 )
1582 .is_err());
1583 }
1584
1585 #[test]
1586 fn test_unknown_property_passes_with_any_value() {
1587 let validator = PropertyValidator::new();
1588 let v = PropertyValue::Length(Length::from_pt(42.0));
1590 assert!(validator.validate(PropertyId::SpaceBefore, &v).is_ok());
1592 }
1593
1594 #[test]
1599 fn test_no_mutual_exclusion_when_neither_present() {
1600 let validator = PropertyValidator::new();
1601 let props: Vec<(PropertyId, PropertyValue)> = vec![];
1602 let errors = validator.check_mutual_exclusion(&props);
1603 assert!(errors.is_empty());
1604 }
1605
1606 #[test]
1607 fn test_mutual_exclusion_only_width() {
1608 let validator = PropertyValidator::new();
1609 let props = vec![(
1610 PropertyId::Width,
1611 PropertyValue::Length(Length::from_pt(100.0)),
1612 )];
1613 let errors = validator.check_mutual_exclusion(&props);
1614 assert!(errors.is_empty());
1615 }
1616
1617 #[test]
1618 fn test_mutual_exclusion_only_height() {
1619 let validator = PropertyValidator::new();
1620 let props = vec![(
1621 PropertyId::Height,
1622 PropertyValue::Length(Length::from_pt(100.0)),
1623 )];
1624 let errors = validator.check_mutual_exclusion(&props);
1625 assert!(errors.is_empty());
1626 }
1627}