deps_npm/
error.rs

1//! Errors specific to npm/JavaScript dependency handling.
2//!
3//! These errors cover parsing package.json files, validating npm semver specifications,
4//! and communicating with the npm registry.
5
6use thiserror::Error;
7
8/// Errors specific to npm/JavaScript dependency handling.
9///
10/// These errors cover parsing package.json files, validating npm semver specifications,
11/// and communicating with the npm registry.
12#[derive(Error, Debug)]
13pub enum NpmError {
14    /// Failed to parse package.json
15    #[error("Failed to parse package.json: {source}")]
16    JsonParseError {
17        #[source]
18        source: serde_json::Error,
19    },
20
21    /// Invalid npm semver version specifier
22    #[error("Invalid npm semver version specifier '{specifier}': {message}")]
23    InvalidVersionSpecifier { specifier: String, message: String },
24
25    /// Package not found on npm registry
26    #[error("Package '{package}' not found on npm registry")]
27    PackageNotFound { package: String },
28
29    /// npm registry request failed
30    #[error("npm registry request failed for '{package}': {source}")]
31    RegistryError {
32        package: String,
33        #[source]
34        source: Box<dyn std::error::Error + Send + Sync>,
35    },
36
37    /// Failed to deserialize npm API response
38    #[error("Failed to parse npm API response for '{package}': {source}")]
39    ApiResponseError {
40        package: String,
41        #[source]
42        source: serde_json::Error,
43    },
44
45    /// Invalid package.json structure
46    #[error("Invalid package.json structure: {message}")]
47    InvalidStructure { message: String },
48
49    /// Missing required field in package.json
50    #[error("Missing required field '{field}' in {section}")]
51    MissingField { section: String, field: String },
52
53    /// Cache error
54    #[error("Cache error: {0}")]
55    CacheError(String),
56
57    /// I/O error
58    #[error("I/O error: {0}")]
59    Io(#[from] std::io::Error),
60
61    /// Generic error wrapper
62    #[error(transparent)]
63    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
64}
65
66/// Result type alias for npm operations.
67pub type Result<T> = std::result::Result<T, NpmError>;
68
69impl NpmError {
70    /// Create a registry error from any error type.
71    pub fn registry_error(
72        package: impl Into<String>,
73        error: impl std::error::Error + Send + Sync + 'static,
74    ) -> Self {
75        Self::RegistryError {
76            package: package.into(),
77            source: Box::new(error),
78        }
79    }
80
81    /// Create an API response error.
82    pub fn api_response_error(package: impl Into<String>, error: serde_json::Error) -> Self {
83        Self::ApiResponseError {
84            package: package.into(),
85            source: error,
86        }
87    }
88
89    /// Create an invalid structure error.
90    pub fn invalid_structure(message: impl Into<String>) -> Self {
91        Self::InvalidStructure {
92            message: message.into(),
93        }
94    }
95
96    /// Create a missing field error.
97    pub fn missing_field(section: impl Into<String>, field: impl Into<String>) -> Self {
98        Self::MissingField {
99            section: section.into(),
100            field: field.into(),
101        }
102    }
103
104    /// Create an invalid version specifier error.
105    pub fn invalid_version_specifier(
106        specifier: impl Into<String>,
107        message: impl Into<String>,
108    ) -> Self {
109        Self::InvalidVersionSpecifier {
110            specifier: specifier.into(),
111            message: message.into(),
112        }
113    }
114}
115
116/// Convert from deps_core::DepsError for compatibility
117impl From<deps_core::DepsError> for NpmError {
118    fn from(err: deps_core::DepsError) -> Self {
119        match err {
120            deps_core::DepsError::ParseError { source, .. } => Self::CacheError(source.to_string()),
121            deps_core::DepsError::CacheError(msg) => Self::CacheError(msg),
122            deps_core::DepsError::InvalidVersionReq(msg) => Self::InvalidVersionSpecifier {
123                specifier: String::new(),
124                message: msg,
125            },
126            deps_core::DepsError::Io(e) => Self::Io(e),
127            deps_core::DepsError::Json(e) => Self::JsonParseError { source: e },
128            other => Self::CacheError(other.to_string()),
129        }
130    }
131}
132
133/// Convert to deps_core::DepsError for interoperability
134impl From<NpmError> for deps_core::DepsError {
135    fn from(err: NpmError) -> Self {
136        match err {
137            NpmError::JsonParseError { source } => Self::Json(source),
138            NpmError::InvalidVersionSpecifier { message, .. } => Self::InvalidVersionReq(message),
139            NpmError::PackageNotFound { package } => {
140                Self::CacheError(format!("Package '{package}' not found"))
141            }
142            NpmError::RegistryError { package, source } => Self::ParseError {
143                file_type: format!("npm registry for {package}"),
144                source,
145            },
146            NpmError::ApiResponseError { source, .. } => Self::Json(source),
147            NpmError::InvalidStructure { message } => Self::CacheError(message),
148            NpmError::MissingField { section, field } => {
149                Self::CacheError(format!("Missing '{field}' in {section}"))
150            }
151            NpmError::CacheError(msg) => Self::CacheError(msg),
152            NpmError::Io(e) => Self::Io(e),
153            NpmError::Other(e) => Self::CacheError(e.to_string()),
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_error_display() {
164        let err = NpmError::PackageNotFound {
165            package: "nonexistent".into(),
166        };
167        assert_eq!(
168            err.to_string(),
169            "Package 'nonexistent' not found on npm registry"
170        );
171
172        let err = NpmError::missing_field("dependencies", "express");
173        assert_eq!(
174            err.to_string(),
175            "Missing required field 'express' in dependencies"
176        );
177
178        let err = NpmError::invalid_structure("missing name field");
179        assert_eq!(
180            err.to_string(),
181            "Invalid package.json structure: missing name field"
182        );
183    }
184
185    #[test]
186    fn test_error_construction() {
187        let err = NpmError::registry_error(
188            "express",
189            std::io::Error::from(std::io::ErrorKind::NotFound),
190        );
191        assert!(matches!(err, NpmError::RegistryError { .. }));
192
193        let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
194        let err = NpmError::api_response_error("lodash", json_err);
195        assert!(matches!(err, NpmError::ApiResponseError { .. }));
196    }
197
198    #[test]
199    fn test_invalid_version_specifier() {
200        let err = NpmError::invalid_version_specifier("invalid", "not a valid semver");
201        assert!(err.to_string().contains("invalid"));
202        assert!(err.to_string().contains("not a valid semver"));
203    }
204
205    #[test]
206    fn test_conversion_to_deps_error() {
207        let npm_err = NpmError::PackageNotFound {
208            package: "test".into(),
209        };
210        let deps_err: deps_core::DepsError = npm_err.into();
211        assert!(deps_err.to_string().contains("not found"));
212    }
213}