Skip to main content

upstream_ontologist/providers/
node.rs

1use crate::{ProviderError, UpstreamDatum, UpstreamDatumWithMetadata, UpstreamMetadata};
2use serde::Deserialize;
3use std::collections::HashMap;
4
5/// Information about a specific NPM package version
6#[derive(Deserialize)]
7pub struct NpmVersion {
8    /// Distribution information for the package
9    #[serde(rename = "dist")]
10    pub dist: NpmDist,
11    /// Package dependencies
12    #[serde(rename = "dependencies")]
13    pub dependencies: Option<HashMap<String, String>>,
14    /// Development dependencies
15    #[serde(rename = "devDependencies")]
16    pub dev_dependencies: Option<HashMap<String, String>>,
17    /// Peer dependencies
18    #[serde(rename = "peerDependencies")]
19    pub peer_dependencies: Option<HashMap<String, String>>,
20    /// Optional dependencies
21    #[serde(rename = "optionalDependencies")]
22    pub optional_dependencies: Option<HashMap<String, String>>,
23    /// Bundled dependencies
24    #[serde(rename = "bundledDependencies")]
25    pub bundled_dependencies: Option<Vec<String>>,
26    /// Engine requirements
27    #[serde(rename = "engines")]
28    pub engines: Option<HashMap<String, String>>,
29    /// NPM scripts
30    #[serde(rename = "scripts")]
31    pub scripts: Option<HashMap<String, String>>,
32    /// Package name
33    pub name: String,
34    /// Package version
35    pub version: String,
36    /// README file name
37    #[serde(rename = "readmeFilename")]
38    pub readme_filename: Option<String>,
39    /// Package maintainers
40    #[serde(rename = "maintainers")]
41    pub maintainers: Vec<NpmPerson>,
42    /// Package author
43    #[serde(rename = "author")]
44    pub author: Option<NpmPerson>,
45    /// Source repository
46    #[serde(rename = "repository")]
47    pub repository: Option<NpmRepository>,
48    /// Bug tracker information
49    #[serde(rename = "bugs")]
50    pub bugs: Option<NpmBugs>,
51    /// Package homepage
52    #[serde(rename = "homepage")]
53    pub homepage: Option<String>,
54    /// Package keywords
55    #[serde(rename = "keywords")]
56    pub keywords: Option<Vec<String>>,
57    /// License identifier
58    #[serde(rename = "license")]
59    pub license: Option<String>,
60}
61
62/// NPM person (author/maintainer) information
63#[derive(Deserialize)]
64pub struct NpmPerson {
65    /// Person's name
66    pub name: String,
67    /// Person's email address
68    pub email: String,
69}
70
71impl From<NpmPerson> for crate::Person {
72    fn from(person: NpmPerson) -> Self {
73        crate::Person {
74            name: Some(person.name),
75            email: Some(person.email),
76            url: None,
77        }
78    }
79}
80
81/// NPM distribution information
82#[derive(Deserialize)]
83pub struct NpmDist {
84    /// SHA checksum of the package
85    pub shasum: String,
86    /// URL to the package tarball
87    pub tarball: String,
88    /// Package integrity string
89    pub integrity: String,
90    /// Package signatures
91    pub signatures: Vec<NpmSignature>,
92}
93
94/// NPM package signature
95#[derive(Deserialize)]
96pub struct NpmSignature {
97    /// Key identifier
98    pub keyid: String,
99    /// Signature string
100    pub sig: String,
101}
102
103/// NPM repository information
104#[derive(Deserialize)]
105pub struct NpmRepository {
106    /// Repository type (e.g., git)
107    #[serde(rename = "type")]
108    pub type_: String,
109    /// Repository URL
110    pub url: String,
111}
112
113/// NPM bug tracker information
114#[derive(Deserialize)]
115pub struct NpmBugs {
116    /// Bug tracker URL
117    pub url: String,
118}
119
120/// Complete NPM package metadata
121#[derive(Deserialize)]
122pub struct NpmPackage {
123    /// Package identifier
124    #[serde(rename = "_id")]
125    pub id: String,
126    /// Package revision
127    #[serde(rename = "_rev")]
128    pub rev: String,
129    /// Package name
130    pub name: String,
131    /// Package description
132    pub description: String,
133    /// Distribution tags mapping
134    #[serde(rename = "dist-tags")]
135    pub dist_tags: HashMap<String, String>,
136    /// All available versions
137    pub versions: HashMap<String, NpmVersion>,
138    /// Package README content
139    pub readme: String,
140    /// Package maintainers
141    pub maintainers: Vec<NpmPerson>,
142    /// Timestamps for various events
143    pub time: HashMap<String, String>,
144    /// Package author
145    pub author: Option<NpmPerson>,
146    /// Source repository
147    pub repository: Option<NpmRepository>,
148    /// Bug tracker information
149    pub bugs: Option<NpmBugs>,
150    /// Package homepage
151    pub homepage: Option<String>,
152    /// Package keywords
153    pub keywords: Option<Vec<String>>,
154    /// License identifier
155    pub license: Option<String>,
156    /// Package dependencies
157    pub dependencies: Option<HashMap<String, String>>,
158    /// Development dependencies
159    #[serde(rename = "devDependencies")]
160    pub dev_dependencies: Option<HashMap<String, String>>,
161    /// Peer dependencies
162    #[serde(rename = "peerDependencies")]
163    pub peer_dependencies: Option<HashMap<String, String>>,
164    /// Optional dependencies
165    #[serde(rename = "optionalDependencies")]
166    pub optional_dependencies: Option<HashMap<String, String>>,
167    /// Bundled dependencies
168    #[serde(rename = "bundledDependencies")]
169    pub bundled_dependencies: Option<Vec<String>>,
170    /// Engine requirements
171    pub engines: Option<HashMap<String, String>>,
172    /// NPM scripts
173    pub scripts: Option<HashMap<String, String>>,
174    /// README file name
175    #[serde(rename = "readmeFilename")]
176    pub readme_filename: Option<String>,
177}
178
179impl TryInto<UpstreamMetadata> for NpmPackage {
180    type Error = ProviderError;
181
182    fn try_into(self) -> Result<UpstreamMetadata, Self::Error> {
183        let mut metadata = UpstreamMetadata::default();
184
185        let package_name = self.name.clone();
186        metadata.insert(UpstreamDatumWithMetadata {
187            datum: UpstreamDatum::Name(self.name),
188            certainty: None,
189            origin: None,
190        });
191
192        metadata.insert(UpstreamDatumWithMetadata {
193            datum: UpstreamDatum::Description(self.description),
194            certainty: None,
195            origin: None,
196        });
197
198        if let Some(homepage) = self.homepage {
199            metadata.insert(UpstreamDatumWithMetadata {
200                datum: UpstreamDatum::Homepage(homepage),
201                certainty: None,
202                origin: None,
203            });
204        }
205
206        if let Some(author) = self.author {
207            metadata.insert(UpstreamDatumWithMetadata {
208                datum: UpstreamDatum::Author(vec![author.into()]),
209                certainty: None,
210                origin: None,
211            });
212        }
213
214        if let Some(repository) = self.repository {
215            metadata.insert(UpstreamDatumWithMetadata {
216                datum: UpstreamDatum::Repository(repository.url),
217                certainty: None,
218                origin: None,
219            });
220        }
221
222        if let Some(bugs) = self.bugs {
223            metadata.insert(UpstreamDatumWithMetadata {
224                datum: UpstreamDatum::BugDatabase(bugs.url),
225                certainty: None,
226                origin: None,
227            });
228        }
229
230        if let Some(license) = self.license {
231            metadata.insert(UpstreamDatumWithMetadata {
232                datum: UpstreamDatum::License(license),
233                certainty: None,
234                origin: None,
235            });
236        }
237
238        if let Some(keywords) = self.keywords {
239            metadata.insert(UpstreamDatumWithMetadata {
240                datum: UpstreamDatum::Keywords(keywords),
241                certainty: None,
242                origin: None,
243            });
244        }
245
246        // Find the latest version
247        if let Some(latest_version) = self.dist_tags.get("latest") {
248            if let Some(version) = self.versions.get(latest_version) {
249                metadata.insert(UpstreamDatumWithMetadata {
250                    datum: UpstreamDatum::Version(version.version.to_string()),
251                    certainty: None,
252                    origin: None,
253                });
254            }
255
256            let version_data = self.versions.get(latest_version).map_or_else(
257                || {
258                    Err(ProviderError::Other(format!(
259                        "Could not find version {} in package {}",
260                        latest_version, &package_name
261                    )))
262                },
263                Ok,
264            )?;
265
266            metadata.insert(UpstreamDatumWithMetadata {
267                datum: UpstreamDatum::Download(version_data.dist.tarball.to_string()),
268                certainty: None,
269                origin: None,
270            });
271        }
272
273        Ok(metadata)
274    }
275}
276
277/// Load NPM package metadata from the registry
278///
279/// Fetches package information from the NPM registry API for the specified package name.
280/// Returns the parsed package metadata or None if the package doesn't exist.
281pub async fn load_npm_package(package: &str) -> Result<Option<NpmPackage>, crate::ProviderError> {
282    let http_url: url::Url = format!("https://registry.npmjs.org/{}", package)
283        .parse()
284        .map_err(|e: url::ParseError| crate::ProviderError::Other(e.to_string()))?;
285    let data = crate::load_json_url(&http_url, None).await?;
286    serde_json::from_value(data)
287        .map_err(|e| crate::ProviderError::ParseError(format!("Failed to parse npm data: {}", e)))
288}
289
290/// Get upstream metadata for an NPM package
291///
292/// Retrieves and converts NPM package information into standardized upstream metadata format.
293/// Returns empty metadata if the package is not found.
294pub async fn remote_npm_metadata(package: &str) -> Result<UpstreamMetadata, ProviderError> {
295    let data = load_npm_package(package).await?;
296
297    match data {
298        Some(data) => data.try_into(),
299        None => Ok(UpstreamMetadata::default()),
300    }
301}
302
303#[cfg(test)]
304mod npm_tests {
305    use super::*;
306
307    #[test]
308    fn test_load_npm_package() {
309        let data = include_str!(".././testdata/npm.json");
310
311        let npm_data: NpmPackage = serde_json::from_str(data).unwrap();
312
313        assert_eq!(npm_data.name, "leftpad");
314    }
315}