1use thiserror::Error;
2
3#[derive(Error, Debug)]
8pub enum PypiError {
9 #[error("Failed to parse pyproject.toml: {source}")]
11 TomlParseError {
12 #[source]
13 source: toml_edit::TomlError,
14 },
15
16 #[error("Invalid PEP 440 version specifier '{specifier}': {source}")]
18 InvalidVersionSpecifier {
19 specifier: String,
20 #[source]
21 source: pep440_rs::VersionSpecifiersParseError,
22 },
23
24 #[error("Invalid PEP 508 dependency specification: {source}")]
26 InvalidDependencySpec {
27 #[source]
28 source: pep508_rs::Pep508Error,
29 },
30
31 #[error("Package '{package}' not found on PyPI")]
33 PackageNotFound { package: String },
34
35 #[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 #[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 #[error("Unsupported dependency format: {message}")]
53 UnsupportedFormat { message: String },
54
55 #[error("Missing required field '{field}' in {section}")]
57 MissingField { section: String, field: String },
58
59 #[error("Cache error: {0}")]
61 CacheError(String),
62
63 #[error(transparent)]
65 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
66}
67
68pub type Result<T> = std::result::Result<T, PypiError>;
70
71impl PypiError {
72 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 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 pub fn unsupported_format(message: impl Into<String>) -> Self {
93 Self::UnsupportedFormat {
94 message: message.into(),
95 }
96 }
97
98 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}