diff --git a/src/args.rs b/src/args.rs index 7a1f5d3..5a52b4f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use clap::{Parser, Subcommand}; +use semver::VersionReq; #[derive(Clone, Debug, Parser)] #[clap(infer_subcommands = true)] @@ -44,8 +45,14 @@ impl FromStr for Version { let version = match &*input { "latest" => Self::Latest, - "lts" => Self::Lts, - _ => Self::SemVer(SemVersion::from_str(s)?), + "lts" => Self::LatestLts, + _ => { + if let Ok(req) = VersionReq::parse(s) { + Self::Req(req) + } else { + Self::Lts(s.to_lowercase()) + } + } }; Ok(version) @@ -55,40 +62,7 @@ impl FromStr for Version { #[derive(Clone, Debug)] pub enum Version { Latest, - Lts, - SemVer(SemVersion), -} - -#[derive(Clone, Debug)] -pub struct SemVersion { - pub major: u8, - pub minor: Option, - pub patch: Option, -} - -impl FromStr for SemVersion { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let mut major = s; - let mut minor = None; - let mut patch = None; - - if let Some((maj, rest)) = s.split_once('.') { - major = maj; - - if let Some((min, pat)) = rest.split_once('.') { - minor = Some(min.parse().map_err(|_| "minor is not a number")?); - patch = Some(pat.parse().map_err(|_| "patch is not a number")?); - } else { - minor = Some(rest.parse().map_err(|_| "minor is not a number")?); - } - } - - Ok(Self { - major: major.parse().map_err(|_| "major is not a number")?, - minor, - patch, - }) - } + LatestLts, + Lts(String), + Req(VersionReq), } diff --git a/src/consts.rs b/src/consts.rs index bd326ee..ad3b9de 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -10,8 +10,11 @@ lazy_static! { pub static ref DATA_DIR: PathBuf = dirs::data_dir() .unwrap_or_else(|| PathBuf::from(".data")) .join(PathBuf::from("nenv")); - pub static ref NODE_PATH: PathBuf = DATA_DIR.join(PathBuf::from("current")); - pub static ref NODE_VERSIONS_PATH: PathBuf = DATA_DIR.join(PathBuf::from("versions")); + pub static ref CACHE_DIR: PathBuf = dirs::cache_dir() + .unwrap_or_else(|| PathBuf::from(".cache")) + .join(PathBuf::from("nenv")); + pub static ref BIN_DIR: PathBuf = DATA_DIR.join(PathBuf::from("bin")); + pub static ref NODE_VERSIONS_DIR: PathBuf = DATA_DIR.join(PathBuf::from("versions")); pub static ref NODE_ARCHIVE_SUFFIX: String = format!("-{OS}-{ARCH}.{ARCHIVE_TYPE}"); } diff --git a/src/download.rs b/src/download.rs deleted file mode 100644 index f654cbe..0000000 --- a/src/download.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::{error::LibResult, Version}; - -pub async fn download_version(version: Version) -> LibResult<()> { - todo!("Download node version to data dir") -} diff --git a/src/error.rs b/src/error.rs index 024df1e..020aa8d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,25 @@ +use std::io; + +use miette::Diagnostic; use thiserror::Error; +use crate::web_api::error::ApiError; + pub(crate) type LibResult = Result; pub(crate) type LibError = Error; pub type Result = std::result::Result; -#[derive(Debug, Error)] -pub enum Error {} +#[derive(Debug, Error, Diagnostic)] +pub enum Error { + #[error("Failed to call nodejs.com api: {0}")] + Web( + #[from] + #[source] + #[diagnostic_source] + ApiError, + ), + + #[error("IO Error: {0}")] + Io(#[from] io::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 571389a..bb2d016 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,15 @@ +use repository::{config::Config, NodeVersion, Repository}; + mod consts; -mod download; pub mod error; -mod utils; +pub mod repository; mod web_api; +use error::Result; -pub enum Version { - Latest, - Lts, - Specific(u8, Option, Option), +pub async fn install_version(version: NodeVersion) -> Result<()> { + get_repository().await?.install_version(version).await } -pub fn install(version: Version) {} +async fn get_repository() -> Result { + Repository::init(Config::default()).await +} diff --git a/src/main.rs b/src/main.rs index a237494..152c8d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,29 @@ use args::Args; use clap::Parser; +use nenv::repository::NodeVersion; + mod args; #[tokio::main(flavor = "current_thread")] async fn main() { color_eyre::install().unwrap(); let args: Args = Args::parse(); - dbg!(args); + match args.commmand { + args::Command::Install(v) => nenv::install_version(version_to_req(v.version)) + .await + .unwrap(), + args::Command::Use(_) => todo!(), + args::Command::Default => todo!(), + args::Command::Version => todo!(), + }; +} + +fn version_to_req(version: args::Version) -> NodeVersion { + match version { + args::Version::Latest => NodeVersion::Latest, + args::Version::LatestLts => NodeVersion::LatestLts, + args::Version::Req(req) => NodeVersion::Req(req), + args::Version::Lts(lts_name) => NodeVersion::Lts(lts_name), + } } diff --git a/src/repository/config.rs b/src/repository/config.rs new file mode 100644 index 0000000..bcdb908 --- /dev/null +++ b/src/repository/config.rs @@ -0,0 +1,13 @@ +pub struct Config { + pub dist_base_url: String, + pub default_version: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + dist_base_url: String::from("https://nodejs.org/dist"), + default_version: String::from("latest"), + } + } +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 0000000..f311a34 --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; + +use semver::{Version, VersionReq}; +use tokio::{ + fs::{self, File}, + io::BufWriter, +}; + +use crate::{ + consts::{BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR}, + error::LibResult, + web_api::{VersionInfo, WebApi}, +}; + +use self::{config::Config, versions::Versions}; + +pub mod config; +pub mod versions; + +pub enum NodeVersion { + Latest, + LatestLts, + Lts(String), + Req(VersionReq), +} + +pub struct Repository { + versions: Versions, + web_api: WebApi, + config: Config, +} + +impl Repository { + /// Initializes a new repository with the given confi + pub async fn init(config: Config) -> LibResult { + Self::create_folders().await?; + let web_api = WebApi::new(&config.dist_base_url); + let all_versions = web_api.get_versions().await?; + + Ok(Self { + config, + web_api, + versions: Versions::new(all_versions), + }) + } + + async fn create_folders() -> LibResult<()> { + let dirs = vec![ + &*CFG_DIR, + &*DATA_DIR, + &*CACHE_DIR, + &*BIN_DIR, + &*NODE_VERSIONS_DIR, + ]; + for dir in dirs { + if !dir.exists() { + fs::create_dir_all(dir).await?; + } + } + + Ok(()) + } + + pub async fn install_version(&self, version_req: NodeVersion) -> LibResult<()> { + let info = self.parse_req(version_req); + let archive_path = self.download_version(&info.version).await?; + + todo!() + } + + async fn download_version(&self, version: &Version) -> LibResult { + let download_path = CACHE_DIR.join(format!("node-v{}{}", version, *NODE_ARCHIVE_SUFFIX)); + let mut download_writer = BufWriter::new(File::create(&download_path).await?); + self.web_api + .download_version(version.to_string(), &mut download_writer) + .await?; + + Ok(download_path) + } + + fn parse_req(&self, version_req: NodeVersion) -> &VersionInfo { + match version_req { + NodeVersion::Latest => self.versions.latest(), + NodeVersion::LatestLts => self.versions.latest_lts(), + NodeVersion::Lts(lts) => self.versions.get_lts(<s).expect("Version not found"), + NodeVersion::Req(req) => self + .versions + .get_fulfilling(&req) + .expect("Version not found"), + } + } +} diff --git a/src/repository/versions.rs b/src/repository/versions.rs new file mode 100644 index 0000000..513e26f --- /dev/null +++ b/src/repository/versions.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use semver::{Version, VersionReq}; + +use crate::web_api::VersionInfo; + +pub struct Versions { + lts_versions: HashMap, + versions: HashMap, +} + +impl Versions { + /// creates a new instance to access version information + pub fn new(all_versions: Vec) -> Self { + let lts_versions = all_versions + .iter() + .filter_map(|v| { + Some(( + v.lts.as_ref()?.to_lowercase(), + VersionReq::parse(&format!("{}", v.version.major)).ok()?, + )) + }) + .collect::>(); + let versions = all_versions + .into_iter() + .map(|v| (v.version.to_owned(), v)) + .collect::>(); + + Self { + lts_versions, + versions, + } + } + + /// Returns the latest known node version + pub fn latest(&self) -> &VersionInfo { + let mut versions = self.versions.keys().collect::>(); + versions.sort(); + + self.versions + .get(versions.last().expect("No known node versions")) + .unwrap() + } + + /// Returns the latest node lts version + pub fn latest_lts(&self) -> &VersionInfo { + let mut versions = self + .lts_versions + .values() + .filter_map(|req| self.get_fulfilling(req)) + .collect::>(); + versions.sort_by_key(|v| &v.version); + versions.last().expect("No known lts node versions") + } + + /// Returns a lts version by name + pub fn get_lts>(&self, lts_name: S) -> Option<&VersionInfo> { + let lts_version = self.lts_versions.get(lts_name.as_ref())?; + self.get_fulfilling(lts_version) + } + + /// Returns any version that fulfills the given requirement + pub fn get_fulfilling(&self, req: &VersionReq) -> Option<&VersionInfo> { + let mut versions = self + .versions + .keys() + .filter(|v| req.matches(v)) + .collect::>(); + versions.sort(); + + self.versions.get(versions.last()?) + } +} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 9e52401..0000000 --- a/src/utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{borrow::Borrow, collections::HashMap, hash::Hash}; - -use crate::web_api::VersionInfo; - -/// Converts the list of versions to a tree for easy version lookup -pub fn convert_version_list_to_tree( - version_infos: Vec, -) -> HashMap>> { - let mut version_map = HashMap::new(); - - for info in version_infos { - let major_map = version_map.get_mut_or_insert(info.version.major, HashMap::new()); - let minor_map = major_map.get_mut_or_insert(info.version.minor, HashMap::new()); - minor_map.insert(info.version.patch, info); - } - - version_map -} - -trait GetOrInsert { - fn get_mut_or_insert(&mut self, key: K, default_value: V) -> &mut V; -} - -impl GetOrInsert for HashMap { - fn get_mut_or_insert(&mut self, key: K, default_value: V) -> &mut V { - if !self.contains_key(&key) { - self.insert(key, default_value); - } - self.get_mut(&key).unwrap() - } -} diff --git a/src/web_api/mod.rs b/src/web_api/mod.rs index ad560c2..ccdf697 100644 --- a/src/web_api/mod.rs +++ b/src/web_api/mod.rs @@ -20,18 +20,18 @@ use tokio::io::{AsyncWrite, AsyncWriteExt}; mod test; #[derive(Clone, Debug)] -pub struct NodejsAccess { +pub struct WebApi { base_url: String, client: Client, } -impl Default for NodejsAccess { +impl Default for WebApi { fn default() -> Self { Self::new("https://nodejs.org/dist") } } -impl NodejsAccess { +impl WebApi { /// Creates a new instance to access the nodejs website pub fn new(base_url: S) -> Self { Self {