use std::fmt;
use crate::compatibility::CompatibilityError;
use crate::description::SkillDescriptionError;
use crate::name::SkillNameError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
MissingFrontmatter,
UnterminatedFrontmatter,
InvalidYaml {
message: String,
},
MissingField {
field: &'static str,
},
InvalidName(SkillNameError),
InvalidDescription(SkillDescriptionError),
InvalidCompatibility(CompatibilityError),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingFrontmatter => {
write!(
f,
"SKILL.md must start with '---' (YAML frontmatter delimiter)"
)
}
Self::UnterminatedFrontmatter => {
write!(f, "frontmatter must end with '---' on its own line")
}
Self::InvalidYaml { message } => {
write!(f, "invalid YAML in frontmatter: {message}")
}
Self::MissingField { field } => {
write!(f, "missing required field '{field}' in frontmatter")
}
Self::InvalidName(e) => write!(f, "invalid name: {e}"),
Self::InvalidDescription(e) => write!(f, "invalid description: {e}"),
Self::InvalidCompatibility(e) => write!(f, "invalid compatibility: {e}"),
}
}
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidName(e) => Some(e),
Self::InvalidDescription(e) => Some(e),
Self::InvalidCompatibility(e) => Some(e),
_ => None,
}
}
}
impl From<SkillNameError> for ParseError {
fn from(e: SkillNameError) -> Self {
Self::InvalidName(e)
}
}
impl From<SkillDescriptionError> for ParseError {
fn from(e: SkillDescriptionError) -> Self {
Self::InvalidDescription(e)
}
}
impl From<CompatibilityError> for ParseError {
fn from(e: CompatibilityError) -> Self {
Self::InvalidCompatibility(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadError {
DirectoryNotFound {
path: String,
},
SkillFileNotFound {
path: String,
},
IoError {
path: String,
kind: std::io::ErrorKind,
message: String,
},
ParseError(ParseError),
NameMismatch {
directory_name: String,
skill_name: String,
},
FileNotFound {
path: String,
},
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DirectoryNotFound { path } => {
write!(f, "skill directory not found: '{path}'")
}
Self::SkillFileNotFound { path } => {
write!(f, "SKILL.md not found in '{path}'")
}
Self::IoError { path, message, .. } => {
write!(f, "error reading '{path}': {message}")
}
Self::ParseError(e) => write!(f, "{e}"),
Self::NameMismatch {
directory_name,
skill_name,
} => {
write!(
f,
"skill name '{skill_name}' must match directory name '{directory_name}' (per specification)"
)
}
Self::FileNotFound { path } => {
write!(f, "file not found: '{path}'")
}
}
}
}
impl std::error::Error for LoadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ParseError(e) => Some(e),
_ => None,
}
}
}
impl From<ParseError> for LoadError {
fn from(e: ParseError) -> Self {
Self::ParseError(e)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn parse_error_display_is_helpful() {
let err = ParseError::MissingFrontmatter;
let msg = err.to_string();
assert!(msg.contains("---"));
assert!(msg.contains("SKILL.md"));
}
#[test]
fn parse_error_invalid_yaml_includes_message() {
let err = ParseError::InvalidYaml {
message: "expected ':'".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("expected ':'"));
}
#[test]
fn parse_error_missing_field_includes_field_name() {
let err = ParseError::MissingField { field: "name" };
let msg = err.to_string();
assert!(msg.contains("name"));
}
#[test]
fn parse_error_source_returns_inner_error() {
let inner = SkillNameError::Empty;
let err = ParseError::InvalidName(inner.clone());
assert!(err.source().is_some());
}
#[test]
fn load_error_display_is_helpful() {
let err = LoadError::DirectoryNotFound {
path: "/path/to/skill".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("/path/to/skill"));
}
#[test]
fn load_error_name_mismatch_is_clear() {
let err = LoadError::NameMismatch {
directory_name: "my-skill".to_string(),
skill_name: "other-skill".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("my-skill"));
assert!(msg.contains("other-skill"));
assert!(msg.contains("must match"));
}
#[test]
fn load_error_source_returns_parse_error() {
let inner = ParseError::MissingFrontmatter;
let err = LoadError::ParseError(inner);
assert!(err.source().is_some());
}
}