deps_pypi/
error.rs

1use thiserror::Error;
2
3/// Errors specific to PyPI/Python dependency handling.
4///
5/// These errors cover parsing pyproject.toml files, validating PEP 440/508 specifications,
6/// and communicating with the PyPI registry.
7#[derive(Error, Debug)]
8pub enum PypiError {
9    /// Failed to parse pyproject.toml
10    #[error("Failed to parse pyproject.toml: {source}")]
11    TomlParseError {
12        #[source]
13        source: toml_edit::TomlError,
14    },
15
16    /// Invalid PEP 440 version specifier
17    #[error("Invalid PEP 440 version specifier '{specifier}': {source}")]
18    InvalidVersionSpecifier {
19        specifier: String,
20        #[source]
21        source: pep440_rs::VersionSpecifiersParseError,
22    },
23
24    /// Invalid PEP 508 dependency specification
25    #[error("Invalid PEP 508 dependency specification: {source}")]
26    InvalidDependencySpec {
27        #[source]
28        source: pep508_rs::Pep508Error,
29    },
30
31    /// Package not found on PyPI
32    #[error("Package '{package}' not found on PyPI")]
33    PackageNotFound { package: String },
34
35    /// PyPI registry request failed
36    #[error("PyPI registry request failed for '{package}': {source}")]
37    RegistryError {
38        package: String,
39        #[source]
40        source: Box<dyn std::error::Error + Send + Sync>,
41    },
42
43    /// Failed to deserialize PyPI API response
44    #[error("Failed to parse PyPI API response for '{package}': {source}")]
45    ApiResponseError {
46        package: String,
47        #[source]
48        source: serde_json::Error,
49    },
50
51    /// Unsupported dependency format
52    #[error("Unsupported dependency format: {message}")]
53    UnsupportedFormat { message: String },
54
55    /// Missing required field in pyproject.toml
56    #[error("Missing required field '{field}' in {section}")]
57    MissingField { section: String, field: String },
58
59    /// Cache error
60    #[error("Cache error: {0}")]
61    CacheError(String),
62
63    /// Generic error wrapper
64    #[error(transparent)]
65    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
66}
67
68/// Result type alias for PyPI operations.
69pub type Result<T> = std::result::Result<T, PypiError>;
70
71impl PypiError {
72    /// Create a registry error from any error type.
73    pub fn registry_error(
74        package: impl Into<String>,
75        error: impl std::error::Error + Send + Sync + 'static,
76    ) -> Self {
77        Self::RegistryError {
78            package: package.into(),
79            source: Box::new(error),
80        }
81    }
82
83    /// Create an API response error.
84    pub fn api_response_error(package: impl Into<String>, error: serde_json::Error) -> Self {
85        Self::ApiResponseError {
86            package: package.into(),
87            source: error,
88        }
89    }
90
91    /// Create an unsupported format error.
92    pub fn unsupported_format(message: impl Into<String>) -> Self {
93        Self::UnsupportedFormat {
94            message: message.into(),
95        }
96    }
97
98    /// Create a missing field error.
99    pub fn missing_field(section: impl Into<String>, field: impl Into<String>) -> Self {
100        Self::MissingField {
101            section: section.into(),
102            field: field.into(),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_error_display() {
113        let err = PypiError::PackageNotFound {
114            package: "nonexistent".into(),
115        };
116        assert_eq!(err.to_string(), "Package 'nonexistent' not found on PyPI");
117
118        let err = PypiError::missing_field("project", "dependencies");
119        assert_eq!(
120            err.to_string(),
121            "Missing required field 'dependencies' in project"
122        );
123
124        let err = PypiError::unsupported_format("invalid table format");
125        assert_eq!(
126            err.to_string(),
127            "Unsupported dependency format: invalid table format"
128        );
129    }
130
131    #[test]
132    fn test_error_construction() {
133        let err = PypiError::registry_error(
134            "requests",
135            std::io::Error::from(std::io::ErrorKind::NotFound),
136        );
137        assert!(matches!(err, PypiError::RegistryError { .. }));
138
139        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
140        let err = PypiError::api_response_error("flask", json_err);
141        assert!(matches!(err, PypiError::ApiResponseError { .. }));
142    }
143}