fastxml 0.8.1

A fast, memory-efficient XML library with XPath and XSD validation support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
//! Advanced facet validation tests.
//!
//! Tests for facet constraints that are not covered in the main validation_test.rs:
//! - minExclusive/maxExclusive
//! - pattern validation
//! - totalDigits
//! - fractionDigits
//! - whitespace handling
//! - invalid pattern (regex compilation error)

use fastxml::schema::xsd::facets::{
    FacetConstraints, FacetError, FacetValidator, WhitespaceHandling,
};

// =============================================================================
// minExclusive/maxExclusive Tests
// =============================================================================

mod exclusive_bounds {
    use super::*;

    #[test]
    fn test_min_exclusive_violation() {
        let mut constraints = FacetConstraints::new();
        constraints.min_exclusive = Some("10".to_string());
        let validator = FacetValidator::new(&constraints);

        // Value equal to bound should fail (exclusive)
        let result = validator.validate("10");
        assert!(
            matches!(result, Err(FacetError::BelowMinExclusive { .. })),
            "Value equal to minExclusive should fail, got: {:?}",
            result
        );

        // Value below bound should fail
        let result = validator.validate("5");
        assert!(
            matches!(result, Err(FacetError::BelowMinExclusive { .. })),
            "Value below minExclusive should fail, got: {:?}",
            result
        );

        // Value above bound should pass
        let result = validator.validate("11");
        assert!(result.is_ok(), "Value above minExclusive should pass");
    }

    #[test]
    fn test_max_exclusive_violation() {
        let mut constraints = FacetConstraints::new();
        constraints.max_exclusive = Some("100".to_string());
        let validator = FacetValidator::new(&constraints);

        // Value equal to bound should fail (exclusive)
        let result = validator.validate("100");
        assert!(
            matches!(result, Err(FacetError::AboveMaxExclusive { .. })),
            "Value equal to maxExclusive should fail, got: {:?}",
            result
        );

        // Value above bound should fail
        let result = validator.validate("150");
        assert!(
            matches!(result, Err(FacetError::AboveMaxExclusive { .. })),
            "Value above maxExclusive should fail, got: {:?}",
            result
        );

        // Value below bound should pass
        let result = validator.validate("99");
        assert!(result.is_ok(), "Value below maxExclusive should pass");
    }

    #[test]
    fn test_exclusive_bounds_combined() {
        let mut constraints = FacetConstraints::new();
        constraints.min_exclusive = Some("0".to_string());
        constraints.max_exclusive = Some("10".to_string());
        let validator = FacetValidator::new(&constraints);

        // Test boundary values
        assert!(
            validator.validate("0").is_err(),
            "Value at minExclusive should fail"
        );
        assert!(
            validator.validate("10").is_err(),
            "Value at maxExclusive should fail"
        );

        // Test valid range
        assert!(validator.validate("1").is_ok(), "Value 1 should pass");
        assert!(validator.validate("5").is_ok(), "Value 5 should pass");
        assert!(validator.validate("9").is_ok(), "Value 9 should pass");
    }

    #[test]
    fn test_exclusive_with_decimal_values() {
        let mut constraints = FacetConstraints::new();
        constraints.min_exclusive = Some("0.0".to_string());
        constraints.max_exclusive = Some("1.0".to_string());
        let validator = FacetValidator::new(&constraints);

        assert!(
            validator.validate("0.0").is_err(),
            "Value at minExclusive should fail"
        );
        assert!(
            validator.validate("1.0").is_err(),
            "Value at maxExclusive should fail"
        );
        assert!(validator.validate("0.5").is_ok(), "Value 0.5 should pass");
        assert!(
            validator.validate("0.001").is_ok(),
            "Value just above 0 should pass"
        );
        assert!(
            validator.validate("0.999").is_ok(),
            "Value just below 1 should pass"
        );
    }
}

// =============================================================================
// Pattern Validation Tests
// =============================================================================

mod pattern_validation {
    use super::*;

    #[test]
    fn test_pattern_violation() {
        let mut constraints = FacetConstraints::new().with_pattern(r"[A-Z]{3}-[0-9]{4}");
        constraints.compile_patterns().unwrap();
        let validator = FacetValidator::new(&constraints);

        // Valid pattern
        let result = validator.validate("ABC-1234");
        assert!(result.is_ok(), "Valid pattern should pass");

        // Invalid patterns
        let result = validator.validate("abc-1234");
        assert!(
            matches!(&result, Err(FacetError::PatternMismatch { value, .. }) if value == "abc-1234"),
            "Lowercase should fail, got: {:?}",
            result
        );

        let result = validator.validate("ABC1234");
        assert!(
            matches!(result, Err(FacetError::PatternMismatch { .. })),
            "Missing hyphen should fail"
        );

        let result = validator.validate("AB-1234");
        assert!(
            matches!(result, Err(FacetError::PatternMismatch { .. })),
            "Too few letters should fail"
        );
    }

    #[test]
    fn test_multiple_patterns_all_must_match() {
        let mut constraints = FacetConstraints::new()
            .with_pattern(r"[a-zA-Z]+") // Must contain only letters
            .with_pattern(r".{5,10}"); // Must be 5-10 characters
        constraints.compile_patterns().unwrap();
        let validator = FacetValidator::new(&constraints);

        // Matches both patterns
        assert!(validator.validate("Hello").is_ok());
        assert!(validator.validate("HelloWorld").is_ok());

        // Too short (fails second pattern)
        let result = validator.validate("Hi");
        assert!(
            matches!(result, Err(FacetError::PatternMismatch { .. })),
            "Too short should fail"
        );

        // Contains numbers (fails first pattern)
        let result = validator.validate("Hello123");
        assert!(
            matches!(result, Err(FacetError::PatternMismatch { .. })),
            "Contains numbers should fail"
        );
    }

    #[test]
    fn test_email_pattern() {
        let mut constraints =
            FacetConstraints::new().with_pattern(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}");
        constraints.compile_patterns().unwrap();
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("test@example.com").is_ok());
        assert!(validator.validate("user.name@domain.co.jp").is_ok());

        assert!(
            validator.validate("invalid-email").is_err(),
            "Missing @ should fail"
        );
        assert!(
            validator.validate("@domain.com").is_err(),
            "Missing local part should fail"
        );
    }

    #[test]
    fn test_invalid_pattern_regex() {
        // Pattern with invalid regex syntax
        let mut constraints = FacetConstraints::new().with_pattern(r"[invalid(");
        // compile_patterns logs a warning but doesn't fail
        let _ = constraints.compile_patterns();

        let validator = FacetValidator::new(&constraints);

        // When patterns are not compiled, validation falls back to on-the-fly compilation
        // which should return InvalidPattern error
        let result = validator.validate("test");
        assert!(
            matches!(result, Err(FacetError::InvalidPattern { .. })),
            "Invalid pattern regex should return InvalidPattern error, got: {:?}",
            result
        );
    }
}

// =============================================================================
// Digit Constraints Tests
// =============================================================================

mod digit_constraints {
    use super::*;

    #[test]
    fn test_total_digits_violation() {
        let constraints = FacetConstraints {
            total_digits: Some(5),
            ..Default::default()
        };
        let validator = FacetValidator::new(&constraints);

        // Valid: within limit
        assert!(validator.validate("12345").is_ok());
        assert!(validator.validate("1234").is_ok());
        assert!(validator.validate("123.45").is_ok()); // 5 significant digits

        // Invalid: too many digits
        let result = validator.validate("123456");
        assert!(
            matches!(result, Err(FacetError::TooManyDigits { found: 6, max: 5 })),
            "6 digits should fail with totalDigits=5, got: {:?}",
            result
        );
    }

    #[test]
    fn test_total_digits_with_leading_zeros() {
        let constraints = FacetConstraints {
            total_digits: Some(3),
            ..Default::default()
        };
        let validator = FacetValidator::new(&constraints);

        // Leading zeros are not significant
        assert!(
            validator.validate("00123").is_ok(),
            "Leading zeros should not count"
        );
        assert!(
            validator.validate("0.123").is_ok(),
            "Leading zero before decimal should not count"
        );
    }

    #[test]
    fn test_fraction_digits_violation() {
        let constraints = FacetConstraints {
            fraction_digits: Some(2),
            ..Default::default()
        };
        let validator = FacetValidator::new(&constraints);

        // Valid
        assert!(validator.validate("1.23").is_ok());
        assert!(validator.validate("1.2").is_ok());
        assert!(validator.validate("1").is_ok());

        // Invalid: too many fraction digits
        let result = validator.validate("1.234");
        assert!(
            matches!(
                result,
                Err(FacetError::TooManyFractionDigits { found: 3, max: 2 })
            ),
            "3 fraction digits should fail with fractionDigits=2, got: {:?}",
            result
        );
    }

    #[test]
    fn test_total_and_fraction_digits_combined() {
        let constraints = FacetConstraints {
            total_digits: Some(5),
            fraction_digits: Some(2),
            ..Default::default()
        };
        let validator = FacetValidator::new(&constraints);

        // Valid
        assert!(validator.validate("123.45").is_ok());
        assert!(validator.validate("12.34").is_ok());

        // Too many total digits
        let result = validator.validate("1234.56");
        assert!(
            matches!(result, Err(FacetError::TooManyDigits { .. })),
            "6 total digits should fail"
        );

        // Too many fraction digits
        let result = validator.validate("12.345");
        assert!(
            matches!(result, Err(FacetError::TooManyFractionDigits { .. })),
            "3 fraction digits should fail"
        );
    }
}

// =============================================================================
// Whitespace Handling Tests
// =============================================================================

mod whitespace_handling {
    use super::*;

    #[test]
    fn test_whitespace_preserve() {
        let constraints = FacetConstraints::new()
            .with_whitespace(WhitespaceHandling::Preserve)
            .with_enumeration(vec!["hello  world"]);
        let validator = FacetValidator::new(&constraints);

        // With preserve, whitespace is kept as-is
        assert!(
            validator.validate("hello  world").is_ok(),
            "Exact match should pass"
        );
        assert!(
            validator.validate("hello world").is_err(),
            "Single space should fail"
        );
    }

    #[test]
    fn test_whitespace_replace() {
        let constraints = FacetConstraints::new()
            .with_whitespace(WhitespaceHandling::Replace)
            .with_enumeration(vec!["hello world"]);
        let validator = FacetValidator::new(&constraints);

        // With replace, tabs and newlines become spaces but consecutive spaces remain
        assert!(
            validator.validate("hello\tworld").is_ok(),
            "Tab should be replaced with space"
        );
        assert!(
            validator.validate("hello\nworld").is_ok(),
            "Newline should be replaced with space"
        );
    }

    #[test]
    fn test_whitespace_collapse() {
        let constraints = FacetConstraints::new()
            .with_whitespace(WhitespaceHandling::Collapse)
            .with_enumeration(vec!["hello world"]);
        let validator = FacetValidator::new(&constraints);

        // With collapse, consecutive whitespace becomes single space, trimmed
        assert!(
            validator.validate("hello  world").is_ok(),
            "Multiple spaces should collapse"
        );
        assert!(
            validator.validate("  hello   world  ").is_ok(),
            "Leading/trailing spaces should be trimmed"
        );
        assert!(
            validator.validate("hello\t\nworld").is_ok(),
            "Mixed whitespace should collapse"
        );
    }

    #[test]
    fn test_whitespace_collapse_with_length() {
        let constraints = FacetConstraints::new()
            .with_whitespace(WhitespaceHandling::Collapse)
            .with_min_length(5)
            .with_max_length(15);
        let validator = FacetValidator::new(&constraints);

        // Length is checked after whitespace normalization
        // "  a  " collapses to "a" (length 1)
        let result = validator.validate("  a  ");
        assert!(
            matches!(result, Err(FacetError::TooShort { .. })),
            "Collapsed string is too short"
        );

        // "hello   world" collapses to "hello world" (length 11)
        assert!(
            validator.validate("hello   world").is_ok(),
            "Collapsed string should be valid length"
        );
    }
}

// =============================================================================
// Combined Facet Edge Cases
// =============================================================================

mod combined_facets {
    use super::*;

    #[test]
    fn test_all_length_facets() {
        // minLength and maxLength together
        let constraints = FacetConstraints::new()
            .with_min_length(3)
            .with_max_length(10);
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("ab").is_err()); // too short
        assert!(validator.validate("abc").is_ok()); // min boundary
        assert!(validator.validate("abcdef").is_ok()); // middle
        assert!(validator.validate("abcdefghij").is_ok()); // max boundary
        assert!(validator.validate("abcdefghijk").is_err()); // too long
    }

    #[test]
    fn test_inclusive_and_exclusive_bounds() {
        // minInclusive with maxExclusive
        let mut constraints = FacetConstraints::new().with_min_inclusive("0");
        constraints.max_exclusive = Some("100".to_string());
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("-1").is_err()); // below minInclusive
        assert!(validator.validate("0").is_ok()); // at minInclusive
        assert!(validator.validate("50").is_ok()); // in range
        assert!(validator.validate("99").is_ok()); // just below maxExclusive
        assert!(validator.validate("100").is_err()); // at maxExclusive (fails)
    }

    #[test]
    fn test_pattern_with_length() {
        let mut constraints = FacetConstraints::new()
            .with_pattern(r"[A-Z]+")
            .with_min_length(2)
            .with_max_length(5);
        constraints.compile_patterns().unwrap();
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("A").is_err()); // too short
        assert!(validator.validate("AB").is_ok()); // valid
        assert!(validator.validate("ABCDE").is_ok()); // max length
        assert!(validator.validate("ABCDEF").is_err()); // too long
        assert!(validator.validate("abc").is_err()); // wrong pattern
    }

    #[test]
    fn test_enumeration_with_whitespace() {
        let constraints = FacetConstraints::new()
            .with_whitespace(WhitespaceHandling::Collapse)
            .with_enumeration(vec!["red", "green", "blue"]);
        let validator = FacetValidator::new(&constraints);

        // Whitespace is collapsed before checking enumeration
        assert!(validator.validate(" red ").is_ok());
        assert!(validator.validate("  green  ").is_ok());
        assert!(validator.validate("\tblue\n").is_ok());
    }
}

// =============================================================================
// Negative Value Tests
// =============================================================================

mod negative_values {
    use super::*;

    #[test]
    fn test_negative_number_with_min_inclusive() {
        let constraints = FacetConstraints::new().with_min_inclusive("-10");
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("-10").is_ok()); // at boundary
        assert!(validator.validate("-5").is_ok()); // above min
        assert!(validator.validate("0").is_ok()); // positive
        assert!(validator.validate("-11").is_err()); // below min
        assert!(validator.validate("-100").is_err()); // well below
    }

    #[test]
    fn test_negative_number_with_max_inclusive() {
        let constraints = FacetConstraints::new().with_max_inclusive("-5");
        let validator = FacetValidator::new(&constraints);

        assert!(validator.validate("-5").is_ok()); // at boundary
        assert!(validator.validate("-10").is_ok()); // below max
        assert!(validator.validate("-100").is_ok()); // well below
        assert!(validator.validate("-4").is_err()); // above max
        assert!(validator.validate("0").is_err()); // positive
    }
}