use std::io;
use std::io::Write;
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
pub struct Registry {
url: String,
}
impl Registry {
const DEFAULT_URL: &'static str = "https://registry.npmjs.org";
pub fn new(url: String) -> Self {
Self { url }
}
pub async fn package(&self, package_name: &str) -> io::Result<PackageRegistryEntry> {
let response = reqwest::get(&format!("{}/{}", self.url, package_name)).await
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
if !response.status().is_success() {
return Err(io::Error::new(io::ErrorKind::Other, "Failed to get package"));
}
response.json().await.map_err(|err| io::Error::new(io::ErrorKind::Other, err))
}
}
impl Default for Registry {
fn default() -> Self {
Self::new(Self::DEFAULT_URL.to_string())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageRegistryEntry {
#[serde(rename = "dist-tags")]
dist_tags: HashMap<String, String>,
versions: HashMap<String, PackageMetadata>,
}
impl PackageRegistryEntry {
pub fn latest(&self) -> Option<&PackageMetadata> {
self.dist_tags.get("latest").and_then(|version| self.versions.get(version))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageMetadata {
name: String,
version: String,
description: Option<String>,
license: Option<String>,
keywords: Option<Vec<String>>,
homepage: Option<String>,
author: Option<NameEmail>,
contributors: Option<Vec<NameEmail>>,
maintainers: Option<Vec<NameEmail>>,
dependencies: Option<HashMap<String, String>>,
#[serde(rename = "devDependencies")]
dev_dependencies: Option<HashMap<String, String>>,
#[serde(rename = "peerDependencies")]
peer_dependencies: Option<HashMap<String, String>>,
#[serde(rename = "optionalDependencies")]
optional_dependencies: Option<HashMap<String, String>>,
engines: Option<HashMap<String, String>>,
#[serde(rename = "engineStrict")]
engine_strict: Option<bool>,
#[serde(rename = "packageManager")]
package_manager: Option<String>,
os: Option<Vec<String>>,
cpu: Option<Vec<String>>,
bin: Option<HashMap<String, String>>,
main: Option<String>,
module: Option<String>,
types: Option<String>,
typings: Option<String>,
scripts: Option<HashMap<String, String>>,
dist: PackageDistribution,
}
impl PackageMetadata {
pub fn archive_file_name(&self) -> String {
if self.dist.tarball.ends_with(".tgz") {
if let Some(file_name) = self.dist.tarball.rsplit('/').next() {
return file_name.to_string();
}
}
return format!("{}-{}.tgz", self.name, self.version);
}
pub async fn download_to<W: Write>(&self, mut writer: W) -> io::Result<()> {
let response = reqwest::get(&self.dist.tarball).await
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
if !response.status().is_success() {
return Err(io::Error::new(io::ErrorKind::Other, "Failed to get package"));
}
let stream = response.bytes().await
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
io::copy(&mut io::Cursor::new(stream), &mut writer)?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NameEmail {
name: String,
email: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageDistribution {
integrity: String,
shasum: String,
tarball: String,
signatures: Vec<PackageDistributionSignature>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageDistributionSignature {
keyid: String,
sig: String,
}