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}