Skip to main content

gts_validator/
error.rs

1//! Error types for GTS validation.
2
3use std::path::PathBuf;
4
5use serde::Serialize;
6
7/// The kind of scan-level failure that prevented a file from being validated.
8#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum ScanErrorKind {
11    /// An I/O error occurred while reading the file.
12    IoError,
13    /// The file exceeded the configured maximum size limit.
14    FileTooLarge,
15    /// The file content could not be parsed as valid JSON.
16    JsonParseError,
17    /// The file content could not be parsed as valid YAML.
18    YamlParseError,
19    /// The file content is not valid UTF-8.
20    InvalidEncoding,
21    /// The resolved path is outside the repository root (symlink escape).
22    OutsideRepository,
23    /// A resource limit (`max_files` or `max_total_bytes`) was reached, truncating the scan.
24    LimitExceeded,
25    /// A directory traversal error (permission denied, loop detected, etc.).
26    WalkError,
27    /// An exclude glob pattern could not be parsed.
28    InvalidExcludePattern,
29}
30
31/// A scan-level error: a file that could not be validated at all.
32///
33/// These are distinct from `ValidationError` (which represents a GTS ID that
34/// was found and failed validation). A `ScanError` means the file could not
35/// even be read or parsed — CI must treat these as failures.
36#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
37#[non_exhaustive]
38pub struct ScanError {
39    /// The file path that could not be scanned.
40    pub file: PathBuf,
41    /// The kind of failure.
42    pub kind: ScanErrorKind,
43    /// Human-readable description of the failure.
44    pub message: String,
45}
46
47impl ScanError {
48    /// Format the error for human-readable output.
49    #[must_use]
50    pub fn format_human_readable(&self) -> String {
51        format!("{}: [scan error] {}", self.file.display(), self.message)
52    }
53}
54
55/// A single validation error found in a documentation/config file.
56#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
57#[non_exhaustive]
58pub struct ValidationError {
59    /// File path where the error was found
60    pub file: PathBuf,
61    /// Line number (1-indexed) — for .md files; 0 for structured files
62    pub line: usize,
63    /// Column number (1-indexed) — for .md files; 0 for structured files
64    pub column: usize,
65    /// JSON path (e.g., "$.properties.type.x-gts-ref") — for .json/.yaml files; empty for .md
66    pub json_path: String,
67    /// The original raw string that was found
68    pub raw_value: String,
69    /// The normalized GTS identifier (after stripping gts://, etc.)
70    pub normalized_id: String,
71    /// Human-readable error description
72    pub error: String,
73    /// Surrounding context (for .md: the line content; for .json/.yaml: the parent key)
74    pub context: String,
75}
76
77impl ValidationError {
78    /// Format the error for human-readable output.
79    ///
80    /// For markdown errors: `{file}:{line}:{column}: {error} [{raw_value}]`
81    /// For JSON/YAML errors: `{file}: {error} [{raw_value}] (at {json_path})`
82    #[must_use]
83    pub fn format_human_readable(&self) -> String {
84        if self.line > 0 && self.column > 0 {
85            // Markdown error with line/column
86            format!(
87                "{}:{}:{}: {} [{}]",
88                self.file.display(),
89                self.line,
90                self.column,
91                self.error,
92                self.raw_value
93            )
94        } else if !self.json_path.is_empty() {
95            // JSON/YAML error with json_path
96            format!(
97                "{}: {} [{}] (at {})",
98                self.file.display(),
99                self.error,
100                self.raw_value,
101                self.json_path
102            )
103        } else {
104            // Fallback: just file and error
105            format!(
106                "{}: {} [{}]",
107                self.file.display(),
108                self.error,
109                self.raw_value
110            )
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::path::PathBuf;
119
120    #[test]
121    fn test_format_markdown_error() {
122        let err = ValidationError {
123            file: PathBuf::from("docs/test.md"),
124            line: 42,
125            column: 10,
126            json_path: String::new(),
127            raw_value: "gts.invalid".to_owned(),
128            normalized_id: "gts.invalid".to_owned(),
129            error: "Invalid GTS ID".to_owned(),
130            context: "Some context".to_owned(),
131        };
132
133        let formatted = err.format_human_readable();
134        assert!(formatted.contains("docs/test.md:42:10"));
135        assert!(formatted.contains("Invalid GTS ID"));
136        assert!(formatted.contains("[gts.invalid]"));
137        assert!(!formatted.contains("(at"));
138    }
139
140    #[test]
141    fn test_format_json_error() {
142        let err = ValidationError {
143            file: PathBuf::from("config/test.json"),
144            line: 0,
145            column: 0,
146            json_path: "$.properties.type.x-gts-ref".to_owned(),
147            raw_value: "gts.invalid".to_owned(),
148            normalized_id: "gts.invalid".to_owned(),
149            error: "Invalid GTS ID".to_owned(),
150            context: "x-gts-ref".to_owned(),
151        };
152
153        let formatted = err.format_human_readable();
154        assert!(formatted.contains("config/test.json"));
155        assert!(formatted.contains("Invalid GTS ID"));
156        assert!(formatted.contains("[gts.invalid]"));
157        assert!(formatted.contains("(at $.properties.type.x-gts-ref)"));
158        assert!(!formatted.contains(":0:0"));
159    }
160}