1use std::path::PathBuf;
4
5use rskit_errors::AppError;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum SkillError {
12 #[error("file I/O failed for {path}: {source}")]
14 Io {
15 path: PathBuf,
17 #[source]
19 source: std::io::Error,
20 },
21 #[error("skill file {path} exceeds size limit of {limit_bytes} bytes")]
23 FileTooLarge {
24 path: PathBuf,
26 limit_bytes: u64,
28 },
29 #[error(
31 "skill assets exceed total size limit of {limit_bytes} bytes while reading {path} ({total_bytes} bytes)"
32 )]
33 AssetsTooLarge {
34 path: PathBuf,
36 total_bytes: u64,
38 limit_bytes: u64,
40 },
41 #[error("skill file {path} is not valid UTF-8: {source}")]
43 InvalidUtf8 {
44 path: PathBuf,
46 #[source]
48 source: std::string::FromUtf8Error,
49 },
50 #[error("invalid skill pack file {path}: {reason}")]
52 InvalidPackFile {
53 path: PathBuf,
55 reason: String,
57 },
58 #[error("manifest parse failed for {path}: {source}")]
60 ParseManifest {
61 path: PathBuf,
63 #[source]
65 source: serde_norway::Error,
66 },
67 #[error("invalid skill manifest: {0}")]
69 InvalidManifest(String),
70 #[error("skill config failed: {0}")]
72 Config(String),
73 #[error("skill verification failed: {0}")]
75 Verification(String),
76 #[error("skill already registered: {0}")]
78 AlreadyRegistered(String),
79 #[error("skill not found: {0}")]
81 NotFound(String),
82}
83
84impl From<SkillError> for AppError {
85 fn from(value: SkillError) -> Self {
86 match value {
87 SkillError::NotFound(name) => AppError::not_found("skill", Some(name.as_str())),
88 SkillError::AlreadyRegistered(name) => {
89 AppError::already_exists(format!("skill {name}"))
90 }
91 SkillError::InvalidManifest(message)
92 | SkillError::Config(message)
93 | SkillError::Verification(message) => AppError::invalid_input("skill", message),
94 SkillError::FileTooLarge { path, limit_bytes } => AppError::invalid_input(
95 "skill",
96 format!(
97 "file {} exceeds size limit of {limit_bytes} bytes",
98 path.display()
99 ),
100 ),
101 SkillError::AssetsTooLarge {
102 path,
103 total_bytes,
104 limit_bytes,
105 } => AppError::invalid_input(
106 "skill",
107 format!(
108 "assets exceed total size limit of {limit_bytes} bytes while reading {} ({total_bytes} bytes)",
109 path.display()
110 ),
111 ),
112 SkillError::InvalidUtf8 { path, source } => AppError::invalid_input(
113 "skill",
114 format!("file {} is not valid UTF-8: {source}", path.display()),
115 )
116 .with_cause(source),
117 SkillError::InvalidPackFile { path, reason } => {
118 AppError::invalid_input("skill", format!("{}: {reason}", path.display()))
119 }
120 SkillError::Io { path, source } => AppError::new(
121 rskit_errors::ErrorCode::Internal,
122 format!("skill I/O failed for {}: {source}", path.display()),
123 )
124 .with_cause(source),
125 SkillError::ParseManifest { path, source } => AppError::new(
126 rskit_errors::ErrorCode::InvalidInput,
127 format!(
128 "skill manifest parse failed for {}: {source}",
129 path.display()
130 ),
131 )
132 .with_cause(source),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use std::path::PathBuf;
140
141 use rskit_errors::{AppError, ErrorCode};
142
143 use super::SkillError;
144
145 #[test]
146 fn aggregate_asset_limit_has_distinct_message_and_app_error() {
147 let error = SkillError::AssetsTooLarge {
148 path: PathBuf::from("references/large.bin"),
149 total_bytes: 65,
150 limit_bytes: 64,
151 };
152
153 assert_eq!(
154 error.to_string(),
155 "skill assets exceed total size limit of 64 bytes while reading references/large.bin (65 bytes)"
156 );
157
158 let app_error = AppError::from(error);
159 assert_eq!(app_error.code(), ErrorCode::InvalidInput);
160 assert_eq!(
161 app_error.message(),
162 "invalid skill: assets exceed total size limit of 64 bytes while reading references/large.bin (65 bytes)"
163 );
164 }
165
166 #[test]
167 fn registry_errors_map_to_expected_app_error_codes() {
168 let not_found = AppError::from(SkillError::NotFound("resize".into()));
169 assert_eq!(not_found.code(), ErrorCode::NotFound);
170 assert!(not_found.message().contains("resize"));
171
172 let duplicate = AppError::from(SkillError::AlreadyRegistered("resize".into()));
173 assert_eq!(duplicate.code(), ErrorCode::AlreadyExists);
174 assert!(duplicate.message().contains("resize"));
175 }
176
177 #[test]
178 fn validation_errors_map_to_invalid_input() {
179 for error in [
180 SkillError::InvalidManifest("missing name".into()),
181 SkillError::Config("bad source".into()),
182 SkillError::Verification("unsigned".into()),
183 ] {
184 let app_error = AppError::from(error);
185 assert_eq!(app_error.code(), ErrorCode::InvalidInput);
186 }
187 }
188
189 #[test]
190 fn file_content_errors_preserve_context_and_causes() {
191 let too_large = AppError::from(SkillError::FileTooLarge {
192 path: PathBuf::from("SKILL.md"),
193 limit_bytes: 8,
194 });
195 assert_eq!(too_large.code(), ErrorCode::InvalidInput);
196 assert!(too_large.message().contains("SKILL.md"));
197
198 let utf8_source = String::from_utf8(vec![0xff]).unwrap_err();
199 let invalid_utf8 = AppError::from(SkillError::InvalidUtf8 {
200 path: PathBuf::from("SKILL.md"),
201 source: utf8_source,
202 });
203 assert_eq!(invalid_utf8.code(), ErrorCode::InvalidInput);
204 assert!(invalid_utf8.cause().is_some());
205 }
206
207 #[test]
208 fn filesystem_and_manifest_parse_errors_preserve_causes() {
209 let io = AppError::from(SkillError::Io {
210 path: PathBuf::from("skill.toml"),
211 source: std::io::Error::other("disk"),
212 });
213 assert_eq!(io.code(), ErrorCode::Internal);
214 assert!(io.cause().is_some());
215
216 let source = serde_norway::from_str::<serde_norway::Value>(": bad").unwrap_err();
217 let parse = AppError::from(SkillError::ParseManifest {
218 path: PathBuf::from("skill.toml"),
219 source,
220 });
221 assert_eq!(parse.code(), ErrorCode::InvalidInput);
222 assert!(parse.cause().is_some());
223 }
224
225 #[test]
226 fn invalid_pack_file_maps_reason_to_invalid_input() {
227 let error = AppError::from(SkillError::InvalidPackFile {
228 path: PathBuf::from("references/link"),
229 reason: "symlinks are not allowed".into(),
230 });
231 assert_eq!(error.code(), ErrorCode::InvalidInput);
232 assert!(
233 error
234 .message()
235 .contains("references/link: symlinks are not allowed")
236 );
237 }
238}