xsd-schema 0.1.0

XML Schema (XSD 1.0/1.1) validator with PSVI and a built-in XPath 2.0 engine
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
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
//! XPath error types.
//!
//! This module defines XPath 2.0 specification error codes as per the W3C XPath 2.0
//! specification. Error codes follow the pattern:
//! - XPST: Static errors detected during parsing/analysis
//! - XPDY: Dynamic errors detected during evaluation
//! - XPTY: Type errors
//! - XQTY: XQuery type errors
//! - FORG: Function and operators errors (general)
//! - FOAR: Arithmetic errors
//! - FOCA: Casting errors
//! - FONS: Namespace errors
//! - FODT: Date/time errors

use thiserror::Error;

/// XPath-specific error type with W3C specification error codes.
#[derive(Debug, Clone, Error)]
pub enum XPathError {
    // ========================================================================
    // Static Errors (XPST)
    // ========================================================================
    /// XPST0003: Syntax error in expression.
    #[error("[XPST0003] Syntax error: {message}")]
    XPST0003 { message: String },

    /// XPST0008: QName is not defined (undefined variable or type).
    #[error("[XPST0008] QName '{qname}' is not defined")]
    XPST0008 { qname: String },

    /// XPST0051: Unknown atomic type in sequence type.
    #[error("[XPST0051] The type name '{type_name}' is not defined as an atomic type")]
    XPST0051 { type_name: String },

    /// XPST0017: Function not found.
    #[error("[XPST0017] Function '{name}/{arity}' not found in namespace '{namespace}'")]
    XPST0017 {
        name: String,
        arity: usize,
        namespace: String,
    },

    /// XPST0081: Prefix cannot be expanded to namespace URI.
    #[error("[XPST0081] Prefix '{prefix}' cannot be expanded to a namespace URI")]
    XPST0081 { prefix: String },

    // ========================================================================
    // Dynamic Errors (XPDY)
    // ========================================================================
    /// XPDY0002: Context item is undefined (with message).
    #[error("[XPDY0002] {message}")]
    XPDY0002 { message: String },

    /// XPDY0050: More than one item where singleton expected.
    #[error("[XPDY0050] More than one item in sequence where single item expected")]
    XPDY0050,

    // ========================================================================
    // Type Errors (XPTY)
    // ========================================================================
    /// XPTY0004: Type mismatch.
    #[error("[XPTY0004] Type mismatch: expected '{expected}', found '{found}'")]
    XPTY0004 { expected: String, found: String },

    /// XPTY0004 variant: Only string literals can be cast to certain types.
    #[error("[XPTY0004] Only string literals can be cast to type '{target_type}'")]
    XPTY0004Cast { target_type: String },

    /// XPTY0018: Path expression result contains both nodes and atomic values.
    #[error("[XPTY0018] Path expression result contains both nodes and atomic values")]
    XPTY0018,

    /// XPTY0019: Step result must not be atomic value in path expression.
    #[error("[XPTY0019] Step result in path expression must not be an atomic value")]
    XPTY0019,

    /// XPTY0020: Context item in axis step is not a node.
    #[error("[XPTY0020] Context item for axis step is not a node")]
    XPTY0020,

    // ========================================================================
    // XQuery Type Errors (XQTY)
    // ========================================================================
    /// XQTY0030: Validate expression argument must be single document or element.
    #[error(
        "[XQTY0030] Validate expression argument must be exactly one document or element node"
    )]
    XQTY0030,

    // ========================================================================
    // XQuery Static Errors (XQST)
    // ========================================================================
    /// XQST0076: Invalid collation URI.
    #[error("[XQST0076] Collation '{collation}' is not supported")]
    XQST0076 { collation: String },

    // ========================================================================
    // Function Errors - General (FORG)
    // ========================================================================
    /// FORG0001: Invalid value for cast/constructor.
    #[error("[FORG0001] Invalid value '{value}' for cast to type '{target_type}'")]
    FORG0001 { value: String, target_type: String },

    /// FORG0003: fn:zero-or-one called with sequence > 1 item.
    #[error("[FORG0003] fn:zero-or-one called with sequence containing more than one item")]
    FORG0003,

    /// FORG0004: fn:one-or-more called with empty sequence.
    #[error("[FORG0004] fn:one-or-more called with empty sequence")]
    FORG0004,

    /// FORG0005: fn:exactly-one called with wrong cardinality.
    #[error(
        "[FORG0005] fn:exactly-one called with sequence containing zero or more than one item"
    )]
    FORG0005,

    /// FORG0006: Invalid argument type for function.
    #[error("[FORG0006] Function '{function}' called with invalid argument type '{arg_type}'")]
    FORG0006Named { function: String, arg_type: String },

    /// FORG0006: General effective boolean value error.
    #[error("[FORG0006] {message}")]
    FORG0006 { message: String },

    /// FORG0008: Both arguments to fn:dateTime have a timezone
    #[error("[FORG0008] Both arguments to fn:dateTime have a timezone")]
    FORG0008,

    // ========================================================================
    // Character/Codepoint Errors (FOCH)
    // ========================================================================
    /// FOCH0001: Invalid codepoint.
    #[error("[FOCH0001] Invalid codepoint '{codepoint}' in codepoints-to-string")]
    FOCH0001 { codepoint: String },

    /// FOCH0002: Unsupported collation.
    #[error("[FOCH0002] Unsupported collation: '{collation}'")]
    FOCH0002 { collation: String },

    /// FOCH0003: Unsupported normalization form.
    #[error("[FOCH0003] Unsupported normalization form '{normalization_form}'")]
    FOCH0003 { normalization_form: String },

    // ========================================================================
    // Arithmetic Errors (FOAR)
    // ========================================================================
    /// FOAR0001: Division by zero.
    #[error("[FOAR0001] Division by zero")]
    FOAR0001,

    /// FOAR0002: Numeric overflow or underflow.
    #[error("[FOAR0002] Numeric operation overflow/underflow")]
    FOAR0002,

    // ========================================================================
    // Casting Errors (FOCA)
    // ========================================================================
    /// FOCA0002: QName has null namespace but non-empty prefix.
    #[error("[FOCA0002] QName '{qname}' has null namespace but non-empty prefix")]
    FOCA0002 { qname: String },

    /// FOCA0003: Input value too large for integer.
    #[error("[FOCA0003] {message}")]
    FOCA0003 { message: String },

    /// FOCA0005: NaN supplied as float/double value.
    #[error("[FOCA0005] NaN supplied as float/double value")]
    FOCA0005,

    // ========================================================================
    // Date/Time Errors (FODT)
    // ========================================================================
    /// FODT0001: Overflow/underflow in date/time operation.
    #[error("[FODT0001] Overflow/underflow in date/time operation")]
    FODT0001,

    /// FODT0002: Overflow/underflow in duration operation.
    #[error("[FODT0002] Overflow/underflow in duration operation")]
    FODT0002,

    /// FODT0003: Invalid timezone value.
    #[error("[FODT0003] Invalid timezone value: {value}")]
    FODT0003 { value: String },

    // ========================================================================
    // XDM Typed Value Errors (FOTY)
    // ========================================================================
    /// FOTY0012: Argument node does not have a typed value.
    #[error("[FOTY0012] Argument node does not have a typed value")]
    FOTY0012,

    // ========================================================================
    // Namespace Errors (FONS)
    // ========================================================================
    /// FONS0005: Base-uri not defined in static context.
    #[error("[FONS0005] Base-uri not defined in the static context")]
    FONS0005,

    // ========================================================================
    // URI Errors (FORG)
    // ========================================================================
    /// FORG0009: Error in resolving a relative URI.
    #[error("[FORG0009] Error in resolving a relative URI against a base URI: '{uri}'")]
    FORG0009 { uri: String },

    // ========================================================================
    // Regex Errors (FORX)
    // ========================================================================
    /// FORX0001: Invalid regular expression flags.
    #[error("[FORX0001] Invalid regular expression flags: '{flags}'")]
    FORX0001 { flags: String },

    /// FORX0002: Invalid regular expression.
    #[error("[FORX0002] Invalid regular expression: '{pattern}'")]
    FORX0002 { pattern: String },

    /// FORX0003: Regular expression matches zero-length string.
    #[error("[FORX0003] Regular expression matches zero-length string: '{pattern}'")]
    FORX0003 { pattern: String },

    /// FORX0004: Invalid replacement string.
    #[error("[FORX0004] Invalid replacement string: '{replacement}'")]
    FORX0004 { replacement: String },

    // ========================================================================
    // Operator Errors
    // ========================================================================
    /// Binary operator not defined for argument types.
    #[error("Operator '{operator}' is not defined for arguments of type '{left_type}' and '{right_type}'")]
    BinaryOperatorNotDefined {
        operator: String,
        left_type: String,
        right_type: String,
    },

    /// Unary operator not defined for argument type.
    #[error("Operator '{operator}' is not defined for argument of type '{arg_type}'")]
    UnaryOperatorNotDefined { operator: String, arg_type: String },

    // ========================================================================
    // Internal/General Errors
    // ========================================================================
    /// Internal error for unexpected failures.
    #[error("XPath error: {0}")]
    Internal(String),
}

impl From<crate::xpath::parser::ParseError> for XPathError {
    fn from(e: crate::xpath::parser::ParseError) -> Self {
        XPathError::XPST0003 {
            message: e.to_string(),
        }
    }
}

impl From<crate::navigator::NavigatorError> for XPathError {
    fn from(e: crate::navigator::NavigatorError) -> Self {
        XPathError::Internal(e.to_string())
    }
}

impl XPathError {
    // ========================================================================
    // Convenience Constructors
    // ========================================================================

    /// Create a new internal XPath error.
    pub fn internal(message: impl Into<String>) -> Self {
        XPathError::Internal(message.into())
    }

    /// Create XPST0003 syntax error.
    pub fn syntax_error(message: impl Into<String>) -> Self {
        XPathError::XPST0003 {
            message: message.into(),
        }
    }

    /// Create XPST0008 undefined QName error.
    pub fn undefined_qname(qname: impl Into<String>) -> Self {
        XPathError::XPST0008 {
            qname: qname.into(),
        }
    }

    /// Create XPST0051 unknown type error.
    pub fn unknown_type(type_name: impl Into<String>) -> Self {
        XPathError::XPST0051 {
            type_name: type_name.into(),
        }
    }

    /// Create XPST0017 function not found error.
    pub fn function_not_found(
        name: impl Into<String>,
        arity: usize,
        namespace: impl Into<String>,
    ) -> Self {
        XPathError::XPST0017 {
            name: name.into(),
            arity,
            namespace: namespace.into(),
        }
    }

    /// Create XPST0081 undefined prefix error.
    pub fn undefined_prefix(prefix: impl Into<String>) -> Self {
        XPathError::XPST0081 {
            prefix: prefix.into(),
        }
    }

    /// Create XPDY0002 context undefined error.
    pub fn context_undefined() -> Self {
        XPathError::XPDY0002 {
            message: "The context item is undefined".to_string(),
        }
    }

    /// Create XPDY0050 more than one item error.
    pub fn more_than_one_item() -> Self {
        XPathError::XPDY0050
    }

    /// Create XPTY0004 type mismatch error.
    pub fn type_mismatch(expected: impl Into<String>, found: impl Into<String>) -> Self {
        XPathError::XPTY0004 {
            expected: expected.into(),
            found: found.into(),
        }
    }

    /// Create XPTY0004 cast-only-from-string error.
    pub fn cast_requires_string_literal(target_type: impl Into<String>) -> Self {
        XPathError::XPTY0004Cast {
            target_type: target_type.into(),
        }
    }

    /// Create XPTY0020 context item not a node error.
    pub fn context_not_a_node() -> Self {
        XPathError::XPTY0020
    }

    /// Create FORG0001 invalid cast value error.
    pub fn invalid_cast_value(value: impl Into<String>, target_type: impl Into<String>) -> Self {
        XPathError::FORG0001 {
            value: value.into(),
            target_type: target_type.into(),
        }
    }

    /// Create FORG0006 invalid argument type error.
    pub fn invalid_argument_type(function: impl Into<String>, arg_type: impl Into<String>) -> Self {
        XPathError::FORG0006Named {
            function: function.into(),
            arg_type: arg_type.into(),
        }
    }

    /// Create a not implemented error.
    pub fn not_implemented(message: impl Into<String>) -> Self {
        XPathError::Internal(format!("Not implemented: {}", message.into()))
    }

    /// Create XPST0017 wrong number of arguments error.
    pub fn wrong_number_of_arguments(function: &str, expected: usize, actual: usize) -> Self {
        XPathError::XPST0017 {
            name: function.to_string(),
            arity: actual,
            namespace: format!(
                "(expected {} argument{})",
                expected,
                if expected == 1 { "" } else { "s" }
            ),
        }
    }

    /// Create binary operator not defined error.
    pub fn binary_operator_not_defined(
        operator: impl Into<String>,
        left_type: impl Into<String>,
        right_type: impl Into<String>,
    ) -> Self {
        XPathError::BinaryOperatorNotDefined {
            operator: operator.into(),
            left_type: left_type.into(),
            right_type: right_type.into(),
        }
    }

    /// Create unary operator not defined error.
    pub fn unary_operator_not_defined(
        operator: impl Into<String>,
        arg_type: impl Into<String>,
    ) -> Self {
        XPathError::UnaryOperatorNotDefined {
            operator: operator.into(),
            arg_type: arg_type.into(),
        }
    }

    /// Create FOTY0012 no typed value error.
    pub fn no_typed_value() -> Self {
        XPathError::FOTY0012
    }

    /// Create FONS0005 base-uri not defined error.
    pub fn base_uri_not_defined() -> Self {
        XPathError::FONS0005
    }

    /// Create FORG0009 URI resolution error.
    pub fn uri_resolution_error(uri: impl Into<String>) -> Self {
        XPathError::FORG0009 { uri: uri.into() }
    }

    /// Create FORX0001 invalid regex flags error.
    pub fn invalid_regex_flags(flags: impl Into<String>) -> Self {
        XPathError::FORX0001 {
            flags: flags.into(),
        }
    }

    /// Create FORX0002 invalid regex pattern error.
    pub fn invalid_regex_pattern(pattern: impl Into<String>) -> Self {
        XPathError::FORX0002 {
            pattern: pattern.into(),
        }
    }

    /// Create FORX0003 regex matches zero-length string error.
    pub fn regex_matches_zero_length(pattern: impl Into<String>) -> Self {
        XPathError::FORX0003 {
            pattern: pattern.into(),
        }
    }

    /// Create FORX0004 invalid replacement string error.
    pub fn invalid_replacement_string(replacement: impl Into<String>) -> Self {
        XPathError::FORX0004 {
            replacement: replacement.into(),
        }
    }

    /// Create XQST0076 unsupported collation error.
    pub fn unsupported_collation(collation: impl Into<String>) -> Self {
        XPathError::XQST0076 {
            collation: collation.into(),
        }
    }

    /// Create FOCH0002 unsupported collation error.
    pub fn unknown_collation(collation: impl Into<String>) -> Self {
        XPathError::FOCH0002 {
            collation: collation.into(),
        }
    }

    /// Get the error code (e.g., "XPTY0004") if this is a spec-defined error.
    pub fn error_code(&self) -> Option<&'static str> {
        match self {
            XPathError::XPST0003 { .. } => Some("XPST0003"),
            XPathError::XPST0008 { .. } => Some("XPST0008"),
            XPathError::XPST0051 { .. } => Some("XPST0051"),
            XPathError::XPST0017 { .. } => Some("XPST0017"),
            XPathError::XPST0081 { .. } => Some("XPST0081"),
            XPathError::XPDY0002 { .. } => Some("XPDY0002"),
            XPathError::XPDY0050 => Some("XPDY0050"),
            XPathError::XPTY0004 { .. } => Some("XPTY0004"),
            XPathError::XPTY0004Cast { .. } => Some("XPTY0004"),
            XPathError::XPTY0018 => Some("XPTY0018"),
            XPathError::XPTY0019 => Some("XPTY0019"),
            XPathError::XPTY0020 => Some("XPTY0020"),
            XPathError::XQTY0030 => Some("XQTY0030"),
            XPathError::XQST0076 { .. } => Some("XQST0076"),
            XPathError::FORG0001 { .. } => Some("FORG0001"),
            XPathError::FORG0003 => Some("FORG0003"),
            XPathError::FORG0004 => Some("FORG0004"),
            XPathError::FORG0005 => Some("FORG0005"),
            XPathError::FORG0006Named { .. } => Some("FORG0006"),
            XPathError::FORG0006 { .. } => Some("FORG0006"),
            XPathError::FORG0008 => Some("FORG0008"),
            XPathError::FOCH0001 { .. } => Some("FOCH0001"),
            XPathError::FOCH0002 { .. } => Some("FOCH0002"),
            XPathError::FOCH0003 { .. } => Some("FOCH0003"),
            XPathError::FOAR0001 => Some("FOAR0001"),
            XPathError::FOAR0002 => Some("FOAR0002"),
            XPathError::FOCA0002 { .. } => Some("FOCA0002"),
            XPathError::FOCA0003 { .. } => Some("FOCA0003"),
            XPathError::FOCA0005 => Some("FOCA0005"),
            XPathError::FODT0001 => Some("FODT0001"),
            XPathError::FODT0002 => Some("FODT0002"),
            XPathError::FODT0003 { .. } => Some("FODT0003"),
            XPathError::FOTY0012 => Some("FOTY0012"),
            XPathError::FONS0005 => Some("FONS0005"),
            XPathError::FORG0009 { .. } => Some("FORG0009"),
            XPathError::FORX0001 { .. } => Some("FORX0001"),
            XPathError::FORX0002 { .. } => Some("FORX0002"),
            XPathError::FORX0003 { .. } => Some("FORX0003"),
            XPathError::FORX0004 { .. } => Some("FORX0004"),
            XPathError::BinaryOperatorNotDefined { .. } => None,
            XPathError::UnaryOperatorNotDefined { .. } => None,
            XPathError::Internal(_) => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_codes() {
        assert_eq!(
            XPathError::context_undefined().error_code(),
            Some("XPDY0002")
        );
        assert_eq!(
            XPathError::more_than_one_item().error_code(),
            Some("XPDY0050")
        );
        assert_eq!(
            XPathError::type_mismatch("xs:integer", "xs:string").error_code(),
            Some("XPTY0004")
        );
    }

    #[test]
    fn test_error_display() {
        let err = XPathError::type_mismatch("xs:integer", "xs:string");
        assert!(err.to_string().contains("XPTY0004"));
        assert!(err.to_string().contains("xs:integer"));
        assert!(err.to_string().contains("xs:string"));
    }

    #[test]
    fn test_internal_error() {
        let err = XPathError::internal("something went wrong");
        assert!(err.to_string().contains("something went wrong"));
        assert_eq!(err.error_code(), None);
    }
}