uplifter 0.1.0

Powerful Node.js package management tool, to replace NPM and Yarn
Documentation
use std::io;
use std::io::Write;
use std::collections::HashMap;

use serde::{Serialize, Deserialize};

/// The registry is the source of packages.
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,
}