1use std::fmt;
4
5use crate::compatibility::CompatibilityError;
6use crate::description::SkillDescriptionError;
7use crate::name::SkillNameError;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum ParseError {
12 MissingFrontmatter,
14 UnterminatedFrontmatter,
16 InvalidYaml {
18 message: String,
20 },
21 MissingField {
23 field: &'static str,
25 },
26 InvalidName(SkillNameError),
28 InvalidDescription(SkillDescriptionError),
30 InvalidCompatibility(CompatibilityError),
32}
33
34impl fmt::Display for ParseError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::MissingFrontmatter => {
38 write!(
39 f,
40 "SKILL.md must start with '---' (YAML frontmatter delimiter)"
41 )
42 }
43 Self::UnterminatedFrontmatter => {
44 write!(f, "frontmatter must end with '---' on its own line")
45 }
46 Self::InvalidYaml { message } => {
47 write!(f, "invalid YAML in frontmatter: {message}")
48 }
49 Self::MissingField { field } => {
50 write!(f, "missing required field '{field}' in frontmatter")
51 }
52 Self::InvalidName(e) => write!(f, "invalid name: {e}"),
53 Self::InvalidDescription(e) => write!(f, "invalid description: {e}"),
54 Self::InvalidCompatibility(e) => write!(f, "invalid compatibility: {e}"),
55 }
56 }
57}
58
59impl std::error::Error for ParseError {
60 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
61 match self {
62 Self::InvalidName(e) => Some(e),
63 Self::InvalidDescription(e) => Some(e),
64 Self::InvalidCompatibility(e) => Some(e),
65 _ => None,
66 }
67 }
68}
69
70impl From<SkillNameError> for ParseError {
71 fn from(e: SkillNameError) -> Self {
72 Self::InvalidName(e)
73 }
74}
75
76impl From<SkillDescriptionError> for ParseError {
77 fn from(e: SkillDescriptionError) -> Self {
78 Self::InvalidDescription(e)
79 }
80}
81
82impl From<CompatibilityError> for ParseError {
83 fn from(e: CompatibilityError) -> Self {
84 Self::InvalidCompatibility(e)
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum LoadError {
91 DirectoryNotFound {
93 path: String,
95 },
96 SkillFileNotFound {
98 path: String,
100 },
101 IoError {
103 path: String,
105 kind: std::io::ErrorKind,
107 message: String,
109 },
110 ParseError(ParseError),
112 NameMismatch {
114 directory_name: String,
116 skill_name: String,
118 },
119 FileNotFound {
121 path: String,
123 },
124}
125
126impl fmt::Display for LoadError {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::DirectoryNotFound { path } => {
130 write!(f, "skill directory not found: '{path}'")
131 }
132 Self::SkillFileNotFound { path } => {
133 write!(f, "SKILL.md not found in '{path}'")
134 }
135 Self::IoError { path, message, .. } => {
136 write!(f, "error reading '{path}': {message}")
137 }
138 Self::ParseError(e) => write!(f, "{e}"),
139 Self::NameMismatch {
140 directory_name,
141 skill_name,
142 } => {
143 write!(
144 f,
145 "skill name '{skill_name}' must match directory name '{directory_name}' (per specification)"
146 )
147 }
148 Self::FileNotFound { path } => {
149 write!(f, "file not found: '{path}'")
150 }
151 }
152 }
153}
154
155impl std::error::Error for LoadError {
156 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157 match self {
158 Self::ParseError(e) => Some(e),
159 _ => None,
160 }
161 }
162}
163
164impl From<ParseError> for LoadError {
165 fn from(e: ParseError) -> Self {
166 Self::ParseError(e)
167 }
168}
169
170#[cfg(test)]
171#[allow(clippy::unwrap_used, clippy::expect_used)]
172mod tests {
173 use super::*;
174 use std::error::Error;
175
176 #[test]
177 fn parse_error_display_is_helpful() {
178 let err = ParseError::MissingFrontmatter;
179 let msg = err.to_string();
180 assert!(msg.contains("---"));
181 assert!(msg.contains("SKILL.md"));
182 }
183
184 #[test]
185 fn parse_error_invalid_yaml_includes_message() {
186 let err = ParseError::InvalidYaml {
187 message: "expected ':'".to_string(),
188 };
189 let msg = err.to_string();
190 assert!(msg.contains("expected ':'"));
191 }
192
193 #[test]
194 fn parse_error_missing_field_includes_field_name() {
195 let err = ParseError::MissingField { field: "name" };
196 let msg = err.to_string();
197 assert!(msg.contains("name"));
198 }
199
200 #[test]
201 fn parse_error_source_returns_inner_error() {
202 let inner = SkillNameError::Empty;
203 let err = ParseError::InvalidName(inner.clone());
204 assert!(err.source().is_some());
205 }
206
207 #[test]
208 fn load_error_display_is_helpful() {
209 let err = LoadError::DirectoryNotFound {
210 path: "/path/to/skill".to_string(),
211 };
212 let msg = err.to_string();
213 assert!(msg.contains("/path/to/skill"));
214 }
215
216 #[test]
217 fn load_error_name_mismatch_is_clear() {
218 let err = LoadError::NameMismatch {
219 directory_name: "my-skill".to_string(),
220 skill_name: "other-skill".to_string(),
221 };
222 let msg = err.to_string();
223 assert!(msg.contains("my-skill"));
224 assert!(msg.contains("other-skill"));
225 assert!(msg.contains("must match"));
226 }
227
228 #[test]
229 fn load_error_source_returns_parse_error() {
230 let inner = ParseError::MissingFrontmatter;
231 let err = LoadError::ParseError(inner);
232 assert!(err.source().is_some());
233 }
234}