diff --git a/Cargo.toml b/Cargo.toml index fe33a18..8b5bec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,6 @@ name = "nenv" version = "0.2.0" edition = "2021" -[lib] -name = "nenv" [[bin]] name = "nenv" @@ -14,7 +12,6 @@ path = "src/main.rs" [dependencies] clap = { version = "4.1.1", features = ["derive"] } -color-eyre = "0.6.2" crossterm = "0.25.0" dialoguer = "0.10.3" dirs = "4.0.0" diff --git a/src/args.rs b/src/args.rs index 5cddefc..401cdda 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; +use crate::repository::NodeVersion; use clap::{Parser, Subcommand}; -use nenv::repository::NodeVersion; #[derive(Clone, Debug, Parser)] #[clap(infer_subcommands = true)] diff --git a/src/error.rs b/src/error.rs index 444287a..9bac734 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,84 +1,128 @@ -use std::io; +use miette::{Diagnostic, NamedSource, SourceSpan}; -use miette::Diagnostic; -use semver::VersionReq; use thiserror::Error; -use crate::{ - mapper::error::MapperError, - repository::{config::ConfigError, extract::ExtractError}, - web_api::error::ApiError, -}; - -pub(crate) type LibResult = Result; - -pub type Result = std::result::Result; +use crate::{mapper::error::MapperError, repository::extract::ExtractError}; #[derive(Debug, Error, Diagnostic)] pub enum Error { - #[diagnostic(code(nenv::web))] - #[error("Failed to call nodejs.com api.")] - Web( - #[from] - #[source] - #[diagnostic_source] - ApiError, - ), - #[diagnostic(code(nenv::extract))] #[error("The node archive could not be extracted")] - Extract( - #[from] - #[source] - #[diagnostic_source] - ExtractError, - ), - - #[diagnostic(code(nenv::config))] - #[error("The config file could not be loaded")] - Config( - #[from] - #[diagnostic_source] - ConfigError, - ), + Extract(#[from] ExtractError), #[diagnostic(code(nenv::mapper))] #[error("Mapping failed")] - Mapper( - #[from] - #[source] - #[diagnostic_source] - MapperError, - ), + Mapper(#[from] MapperError), #[diagnostic(code(nenv::version))] #[error("The passed version is invalid")] - Version( - #[from] - #[diagnostic_source] - VersionError, - ), - - #[diagnostic(code(nenv::json))] - #[error("Failed to work with json")] - Json(#[from] serde_json::Error), - - #[diagnostic(code(nenv::io))] - #[error("Error during IO operation")] - Io(#[from] io::Error), + Version(#[from] VersionError), +} + +#[derive(Debug, Error, Diagnostic)] +#[error("{detail}")] +#[diagnostic(code(nenv::version), help("Make sure there's no typo in the version."))] +pub struct VersionError { + #[source_code] + src: String, + + #[label("this version")] + pos: SourceSpan, + + detail: String, +} + +impl VersionError { + pub fn new(src: S1, detail: S2) -> Self { + let src = src.to_string(); + let pos = (0, src.len()).into(); + + Self { + src, + detail: detail.to_string(), + pos, + } + } + + pub fn unknown_version(src: S) -> Self { + Self::new(src, "unknown version") + } + + pub fn unfulfillable_version(src: S) -> Self { + Self::new(src, "the version requirement cannot be fulfilled") + } + + pub fn not_installed(src: S) -> Self { + Self::new(src, "the version is not installed") + } } #[derive(Debug, Error, Diagnostic)] -pub enum VersionError { - #[error("Invalid version string `{0}`")] - ParseVersion(#[source_code] String), +#[error("failed to parse json")] +#[diagnostic(code(nenv::json::deserialize))] +pub struct ParseJsonError { + #[source_code] + pub src: NamedSource, - #[error("Unknown Version `{0}`")] - UnkownVersion(#[source_code] String), + #[label] + pub pos: SourceSpan, - #[error("The version `{0}` is not installed")] - NotInstalled(#[source_code] String), + #[source] + pub caused_by: serde_json::Error, +} + +#[derive(Debug, Error, Diagnostic)] +#[diagnostic(code(nenv::json::serialize))] +#[error("failed to serialize value to json string")] +pub struct SerializeJsonError { + #[from] + caused_by: serde_json::Error, +} + +#[derive(Debug, Error, Diagnostic)] +#[diagnostic(code(nenv::toml::deserialize))] +#[error("failed to parse toml value")] +pub struct ParseTomlError { + #[source_code] + src: NamedSource, + + #[label] + pos: Option, - #[error("The version requirement `{0}` cannot be fulfilled")] - Unfulfillable(VersionReq), + #[source] + caused_by: toml::de::Error, } + +impl ParseTomlError { + pub fn new(file_name: &str, src: String, caused_by: toml::de::Error) -> Self { + let abs_pos = caused_by + .line_col() + .map(|(l, c)| { + src.lines() + .into_iter() + .take(l) + .map(|line| line.len() + 1) + .sum::() + + c + }) + .map(|p| SourceSpan::new(p.into(), 0.into())); + Self { + src: NamedSource::new(file_name, src), + pos: abs_pos.into(), + caused_by, + } + } +} + +#[derive(Debug, Error, Diagnostic)] +#[diagnostic(code(nenv::toml::serialize))] +#[error("failed to serialize value to toml string")] +pub struct SerializeTomlError { + #[from] + caused_by: toml::ser::Error, +} + +#[derive(Debug, Error, Diagnostic)] +#[diagnostic(code(nenv::http))] +#[error("http request failed")] +pub struct ReqwestError(#[from] reqwest::Error); diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 28dce04..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::ffi::OsString; - -use consts::VERSION_FILE_PATH; -use crossterm::style::Stylize; -use mapper::Mapper; -use repository::{config::Config, NodeVersion, Repository}; - -mod consts; -pub mod error; -pub mod mapper; -pub mod repository; -mod utils; -mod web_api; -use dialoguer::Confirm; -use error::Result; -use tokio::fs; - -use crate::error::VersionError; - -pub async fn install_version(version: NodeVersion) -> Result<()> { - if VERSION_FILE_PATH.exists() { - fs::remove_file(&*VERSION_FILE_PATH).await?; - } - let repo = get_repository().await?; - - if repo.is_installed(&version)? { - if !Confirm::new() - .with_prompt("The version {version} is already installed. Reinstall?") - .default(false) - .interact() - .unwrap() - { - return Ok(()); - } - } - repo.install_version(&version).await?; - println!("Installed {}", version.to_string().bold()); - - Ok(()) -} - -pub async fn set_default_version(version: NodeVersion) -> Result<()> { - let mut mapper = get_mapper().await?; - - if !mapper.repository().is_installed(&version)? - && Confirm::new() - .with_prompt(format!( - "The version {version} is not installed. Do you want to install it?" - )) - .default(false) - .interact() - .unwrap() - { - mapper.repository().install_version(&version).await?; - } - - mapper.set_default_version(&version).await?; - println!("Now using {}", version.to_string().bold()); - - Ok(()) -} - -#[inline] -pub async fn exec(command: String, args: Vec) -> Result { - let mapper = get_mapper().await?; - let active_version = mapper.active_version(); - - if !mapper.repository().is_installed(active_version)? { - mapper.repository().install_version(&active_version).await?; - } - let exit_status = mapper.exec(command, args).await?; - - Ok(exit_status.code().unwrap_or(0)) -} - -pub async fn refresh() -> Result<()> { - get_mapper().await?.remap().await?; - fs::remove_file(&*VERSION_FILE_PATH).await?; - - Ok(()) -} - -pub async fn list_versions() -> Result<()> { - let mapper = get_mapper().await?; - let versions = mapper.repository().installed_versions().await?; - let active_version = mapper - .repository() - .lookup_version(mapper.active_version())?; - - println!("{}", "Installed versions:".bold()); - - for version in versions { - let info = mapper - .repository() - .all_versions() - .get(&version) - .ok_or_else(|| VersionError::UnkownVersion(version.to_string()))?; - let lts = info - .lts - .as_ref() - .map(|l| format!(" ({})", l.to_owned().green())) - .unwrap_or_default(); - - if version == active_version.version { - println!(" {}{} [current]", version.to_string().blue().bold(), lts) - } else { - println!(" {}{}", version.to_string().blue(), lts) - } - } - - Ok(()) -} - -async fn get_repository() -> Result { - Repository::init(Config::load().await?).await -} - -async fn get_mapper() -> Result { - Ok(Mapper::load(get_repository().await?).await) -} diff --git a/src/main.rs b/src/main.rs index 4fa8ecc..00de511 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,25 +2,43 @@ use std::process; use args::Args; use clap::Parser; +use std::ffi::OsString; + +use consts::VERSION_FILE_PATH; +use crossterm::style::Stylize; +use mapper::Mapper; +use repository::{config::Config, NodeVersion, Repository}; + +mod consts; +pub mod error; +pub mod mapper; +pub mod repository; +mod utils; +mod web_api; +use dialoguer::Confirm; +use miette::{IntoDiagnostic, Result}; +use tokio::fs; + +use crate::error::VersionError; mod args; #[tokio::main(flavor = "current_thread")] -async fn main() -> miette::Result<()> { +async fn main() -> Result<()> { miette::set_panic_hook(); let args: Args = Args::parse(); match args.commmand { args::Command::Version => Ok(print_version()), - args::Command::Install(v) => nenv::install_version(v.version).await, - args::Command::Default(v) => nenv::set_default_version(v.version).await, + args::Command::Install(v) => install_version(v.version).await, + args::Command::Default(v) => set_default_version(v.version).await, args::Command::Exec(args) => { - let exit_code = nenv::exec(args.command, args.args).await?; + let exit_code = exec(args.command, args.args).await?; process::exit(exit_code); } - args::Command::Refresh => nenv::refresh().await, - args::Command::ListVersions => nenv::list_versions().await, + args::Command::Refresh => refresh().await, + args::Command::ListVersions => list_versions().await, }?; Ok(()) @@ -29,3 +47,113 @@ async fn main() -> miette::Result<()> { fn print_version() { println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); } + +pub async fn install_version(version: NodeVersion) -> Result<()> { + if VERSION_FILE_PATH.exists() { + fs::remove_file(&*VERSION_FILE_PATH) + .await + .into_diagnostic()?; + } + let repo = get_repository().await?; + + if repo.is_installed(&version)? { + if !Confirm::new() + .with_prompt(format!( + "The version {} is already installed. Reinstall?", + version.to_string().bold() + )) + .default(false) + .interact() + .unwrap() + { + return Ok(()); + } + } + repo.install_version(&version).await?; + println!("Installed {}", version.to_string().bold()); + + Ok(()) +} + +pub async fn set_default_version(version: NodeVersion) -> Result<()> { + let mut mapper = get_mapper().await?; + + if !mapper.repository().is_installed(&version)? + && Confirm::new() + .with_prompt(format!( + "The version {version} is not installed. Do you want to install it?" + )) + .default(false) + .interact() + .unwrap() + { + mapper.repository().install_version(&version).await?; + } + + mapper.set_default_version(&version).await?; + println!("Now using {}", version.to_string().bold()); + + Ok(()) +} + +#[inline] +pub async fn exec(command: String, args: Vec) -> Result { + let mapper = get_mapper().await?; + let active_version = mapper.active_version(); + + if !mapper.repository().is_installed(active_version)? { + mapper.repository().install_version(&active_version).await?; + } + let exit_status = mapper.exec(command, args).await?; + + Ok(exit_status.code().unwrap_or(0)) +} + +pub async fn refresh() -> Result<()> { + get_mapper().await?.remap().await?; + fs::remove_file(&*VERSION_FILE_PATH) + .await + .into_diagnostic()?; + println!("Remapped binaries and cleared version cache"); + + Ok(()) +} + +pub async fn list_versions() -> Result<()> { + let mapper = get_mapper().await?; + let versions = mapper.repository().installed_versions().await?; + let active_version = mapper + .repository() + .lookup_version(mapper.active_version())?; + + println!("{}", "Installed versions:".bold()); + + for version in versions { + let info = mapper + .repository() + .all_versions() + .get(&version) + .ok_or_else(|| VersionError::unknown_version(version.to_string()))?; + let lts = info + .lts + .as_ref() + .map(|l| format!(" ({})", l.to_owned().green())) + .unwrap_or_default(); + + if version == active_version.version { + println!(" {}{} [current]", version.to_string().blue().bold(), lts) + } else { + println!(" {}{}", version.to_string().blue(), lts) + } + } + + Ok(()) +} + +async fn get_repository() -> Result { + Repository::init(Config::load().await?).await +} + +async fn get_mapper() -> Result { + Ok(Mapper::load(get_repository().await?).await) +} diff --git a/src/mapper/error.rs b/src/mapper/error.rs index f565096..999f804 100644 --- a/src/mapper/error.rs +++ b/src/mapper/error.rs @@ -3,22 +3,12 @@ use std::{io, path::PathBuf}; use miette::Diagnostic; use thiserror::Error; -use crate::repository::config::ConfigError; - use super::mapped_command::CommandError; pub type MapperResult = Result; #[derive(Error, Diagnostic, Debug)] pub enum MapperError { - #[error("Config error: {0}")] - Config( - #[from] - #[source] - #[diagnostic_source] - ConfigError, - ), - #[error("Failed to execute mapped command")] Command(#[from] CommandError), diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index 3f03271..e54f0ee 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -4,7 +4,7 @@ use tokio::fs; use crate::{ consts::BIN_DIR, - error::{LibResult, VersionError}, + error::VersionError, repository::{NodeVersion, Repository}, }; @@ -12,6 +12,7 @@ use self::{ error::MapperError, mapped_command::MappedCommand, mapped_dir::map_node_bin, package_info::PackageInfo, }; +use miette::{IntoDiagnostic, Result}; pub mod error; mod mapped_command; @@ -41,7 +42,7 @@ impl Mapper { } /// Sets the given version as the default one - pub async fn set_default_version(&mut self, version: &NodeVersion) -> LibResult<()> { + pub async fn set_default_version(&mut self, version: &NodeVersion) -> Result<()> { self.repo .config .set_default_version(version.clone()) @@ -57,11 +58,11 @@ impl Mapper { } /// Executes a mapped command with the given node environment - pub async fn exec(&self, command: String, args: Vec) -> LibResult { + pub async fn exec(&self, command: String, args: Vec) -> Result { let node_path = self .repo .get_version_path(&self.active_version)? - .expect("version not installed"); + .ok_or_else(|| VersionError::not_installed(&self.active_version))?; let executable = node_path.bin().join(command); let exit_status = MappedCommand::new(executable, args) .run() @@ -73,9 +74,9 @@ impl Mapper { } /// Recreates all environment mappings - pub async fn remap(&self) -> LibResult<()> { - fs::remove_dir_all(&*BIN_DIR).await?; - fs::create_dir_all(&*BIN_DIR).await?; + pub async fn remap(&self) -> Result<()> { + fs::remove_dir_all(&*BIN_DIR).await.into_diagnostic()?; + fs::create_dir_all(&*BIN_DIR).await.into_diagnostic()?; self.map_active_version().await?; Ok(()) @@ -98,11 +99,11 @@ impl Mapper { } /// creates wrapper scripts for the current version - async fn map_active_version(&self) -> LibResult<()> { + async fn map_active_version(&self) -> Result<()> { let dir = self .repo .get_version_path(&self.active_version)? - .ok_or_else(|| VersionError::NotInstalled(self.active_version.to_string()))?; + .ok_or_else(|| VersionError::not_installed(self.active_version.to_string()))?; map_node_bin(dir).await?; Ok(()) diff --git a/src/mapper/package_info.rs b/src/mapper/package_info.rs index 3b5e9bf..a07492f 100644 --- a/src/mapper/package_info.rs +++ b/src/mapper/package_info.rs @@ -1,11 +1,12 @@ use std::{collections::HashMap, path::Path}; +use miette::{IntoDiagnostic, NamedSource, Result}; use semver::VersionReq; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::fs; -use crate::error::LibResult; +use crate::error::ParseJsonError; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PackageInfo { @@ -24,8 +25,8 @@ pub struct EngineInfo { } impl PackageInfo { - pub async fn find() -> LibResult> { - let mut dir = std::env::current_dir()?; + pub async fn find() -> Result> { + let mut dir = std::env::current_dir().into_diagnostic()?; let file_path = dir.join("package.json"); if file_path.exists() { @@ -47,9 +48,14 @@ impl PackageInfo { } /// Loads the package.json config file - pub async fn load(path: &Path) -> LibResult { - let file_content = fs::read_to_string(&path).await?; - let cfg = serde_json::from_str(&file_content)?; + pub async fn load(path: &Path) -> Result { + let file_content = fs::read_to_string(&path).await.into_diagnostic()?; + + let cfg = serde_json::from_str(&file_content).map_err(|e| ParseJsonError { + src: NamedSource::new(path.file_name().unwrap().to_string_lossy(), file_content), + pos: (e.column(), e.column()).into(), + caused_by: e, + })?; Ok(cfg) } diff --git a/src/repository/config.rs b/src/repository/config.rs index f39a07d..b39e668 100644 --- a/src/repository/config.rs +++ b/src/repository/config.rs @@ -1,11 +1,13 @@ -use std::io; - -use miette::{Diagnostic, NamedSource, SourceSpan}; +use miette::Context; +use miette::{IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; -use thiserror::Error; use tokio::fs; -use crate::consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL}; +use crate::error::SerializeTomlError; +use crate::{ + consts::{CFG_DIR, CFG_FILE_PATH, NODE_DIST_URL}, + error::ParseTomlError, +}; use super::NodeVersion; @@ -16,39 +18,6 @@ pub struct Config { pub default_version: NodeVersion, } -pub type ConfigResult = Result; - -#[derive(Error, Diagnostic, Debug)] -pub enum ConfigError { - #[diagnostic(code(nenv::config::io))] - #[error("IO Error: {0}")] - Io( - #[from] - #[source] - io::Error, - ), - #[diagnostic(code(nenv::config::parse))] - #[error("Failed to parse config file")] - #[diagnostic_source] - Parse { - #[source_code] - src: NamedSource, - - #[label] - pos: Option<(usize, usize)>, - - #[source] - e: toml::de::Error, - }, - #[diagnostic(code(nenv::config::write))] - #[error("Failed to serialize config file: {0}")] - Serialize( - #[from] - #[source] - toml::ser::Error, - ), -} - impl Default for Config { fn default() -> Self { Self { @@ -60,34 +29,44 @@ impl Default for Config { impl Config { /// Loads the config file from the default config path - pub async fn load() -> ConfigResult { + pub async fn load() -> Result { if !CFG_FILE_PATH.exists() { if !CFG_DIR.exists() { - fs::create_dir_all(&*CFG_DIR).await?; + fs::create_dir_all(&*CFG_DIR) + .await + .into_diagnostic() + .context("creating config dir")?; } let cfg = Config::default(); cfg.save().await?; Ok(cfg) } else { - let cfg_string = fs::read_to_string(&*CFG_FILE_PATH).await?; - let cfg = toml::from_str(&cfg_string).map_err(|e| ConfigError::Parse { - src: NamedSource::new("config.toml", cfg_string), - pos: e.line_col(), - e, - })?; + let cfg_string = fs::read_to_string(&*CFG_FILE_PATH) + .await + .into_diagnostic() + .context("reading config file")?; + + let cfg = toml::from_str(&cfg_string) + .map_err(|e| ParseTomlError::new("config.toml", cfg_string, e))?; Ok(cfg) } } - pub async fn save(&self) -> ConfigResult<()> { - fs::write(&*CFG_FILE_PATH, toml::to_string_pretty(&self)?).await?; + pub async fn save(&self) -> Result<()> { + fs::write( + &*CFG_FILE_PATH, + toml::to_string_pretty(&self).map_err(SerializeTomlError::from)?, + ) + .await + .into_diagnostic() + .context("writing config file")?; Ok(()) } - pub async fn set_default_version(&mut self, default_version: NodeVersion) -> ConfigResult<()> { + pub async fn set_default_version(&mut self, default_version: NodeVersion) -> Result<()> { self.default_version = default_version; self.save().await } diff --git a/src/repository/extract.rs b/src/repository/extract.rs index 68c043c..2c33776 100644 --- a/src/repository/extract.rs +++ b/src/repository/extract.rs @@ -5,25 +5,22 @@ use std::{ }; use miette::Diagnostic; +use miette::Result; use thiserror::Error; use crate::utils::progress_spinner; type ExtractResult = Result; +/// An error that can occur during extraction #[derive(Error, Debug, Diagnostic)] pub enum ExtractError { + #[diagnostic(code(nenv::extract::io))] #[error("IO error when extracting: {0}")] - Io( - #[from] - #[source] - io::Error, - ), + Io(#[from] io::Error), + + #[diagnostic(code(nenv::extract::zip))] #[error("Failed to extract zip: {0}")] - Zip( - #[from] - #[source] - zip::result::ZipError, - ), + Zip(#[from] zip::result::ZipError), } pub fn extract_file(src: &Path, dst: &Path) -> ExtractResult<()> { diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 1abbdf0..8d9c59c 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -15,10 +15,12 @@ use crate::{ consts::{ ARCH, BIN_DIR, CACHE_DIR, CFG_DIR, DATA_DIR, NODE_ARCHIVE_SUFFIX, NODE_VERSIONS_DIR, OS, }, - error::{LibResult, VersionError}, + error::VersionError, web_api::{VersionInfo, WebApi}, }; +use miette::{IntoDiagnostic, Result}; + use self::{config::Config, node_path::NodePath, versions::Versions}; pub mod config; @@ -87,7 +89,7 @@ pub struct Repository { impl Repository { /// Initializes a new repository with the given confi - pub async fn init(config: Config) -> LibResult { + pub async fn init(config: Config) -> Result { Self::create_folders().await?; let web_api = WebApi::new(&config.dist_base_url); let versions = load_versions(&web_api).await?; @@ -99,7 +101,7 @@ impl Repository { }) } - async fn create_folders() -> LibResult<()> { + async fn create_folders() -> Result<()> { let dirs = vec![ &*CFG_DIR, &*DATA_DIR, @@ -109,7 +111,7 @@ impl Repository { ]; for dir in dirs { if !dir.exists() { - fs::create_dir_all(dir).await?; + fs::create_dir_all(dir).await.into_diagnostic()?; } } @@ -117,7 +119,7 @@ impl Repository { } /// Returns the path for the given node version - pub fn get_version_path(&self, version: &NodeVersion) -> LibResult> { + pub fn get_version_path(&self, version: &NodeVersion) -> Result> { let info = self.lookup_version(&version)?; let path = build_version_path(&info.version); @@ -129,11 +131,11 @@ impl Repository { } /// Returns a list of installed versions - pub async fn installed_versions(&self) -> LibResult> { + pub async fn installed_versions(&self) -> Result> { let mut versions = Vec::new(); - let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await?; + let mut iter = fs::read_dir(&*NODE_VERSIONS_DIR).await.into_diagnostic()?; - while let Some(entry) = iter.next_entry().await? { + while let Some(entry) = iter.next_entry().await.into_diagnostic()? { if let Ok(version) = Version::parse(entry.file_name().to_string_lossy().as_ref()) { versions.push(version); }; @@ -143,7 +145,7 @@ impl Repository { } /// Returns if the given version is installed - pub fn is_installed(&self, version: &NodeVersion) -> LibResult { + pub fn is_installed(&self, version: &NodeVersion) -> Result { let info = self.lookup_version(version)?; Ok(build_version_path(&info.version).exists()) @@ -157,11 +159,11 @@ impl Repository { NodeVersion::Lts(lts) => self .versions .get_lts(<s) - .ok_or_else(|| VersionError::UnkownVersion(lts.to_owned()))?, + .ok_or_else(|| VersionError::unknown_version(lts.to_owned()))?, NodeVersion::Req(req) => self .versions .get_fulfilling(&req) - .ok_or_else(|| VersionError::Unfulfillable(req.to_owned()))?, + .ok_or_else(|| VersionError::unfulfillable_version(req.to_owned()))?, }; Ok(version) @@ -173,7 +175,7 @@ impl Repository { } /// Installs a specified node version - pub async fn install_version(&self, version_req: &NodeVersion) -> LibResult<()> { + pub async fn install_version(&self, version_req: &NodeVersion) -> Result<()> { let info = self.lookup_version(&version_req)?; let archive_path = self.download_version(&info.version).await?; self.extract_archive(info, &archive_path)?; @@ -181,13 +183,14 @@ impl Repository { Ok(()) } - async fn download_version(&self, version: &Version) -> LibResult { + async fn download_version(&self, version: &Version) -> Result { let download_path = CACHE_DIR.join(format!("node-v{}{}", version, *NODE_ARCHIVE_SUFFIX)); if download_path.exists() { return Ok(download_path); } - let mut download_writer = BufWriter::new(File::create(&download_path).await?); + let mut download_writer = + BufWriter::new(File::create(&download_path).await.into_diagnostic()?); self.web_api .download_version(version.to_string(), &mut download_writer) .await?; @@ -195,7 +198,7 @@ impl Repository { Ok(download_path) } - fn extract_archive(&self, info: &VersionInfo, archive_path: &Path) -> LibResult<()> { + fn extract_archive(&self, info: &VersionInfo, archive_path: &Path) -> Result<()> { let dst_path = NODE_VERSIONS_DIR.join(info.version.to_string()); extract::extract_file(archive_path, &dst_path)?; @@ -204,7 +207,7 @@ impl Repository { } #[inline] -async fn load_versions(web_api: &WebApi) -> Result { +async fn load_versions(web_api: &WebApi) -> Result { let versions = if let Some(v) = Versions::load().await { v } else { diff --git a/src/repository/versions.rs b/src/repository/versions.rs index eb6716b..6edb555 100644 --- a/src/repository/versions.rs +++ b/src/repository/versions.rs @@ -4,7 +4,8 @@ use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use tokio::fs; -use crate::{consts::VERSION_FILE_PATH, error::LibResult, web_api::VersionInfo}; +use crate::{consts::VERSION_FILE_PATH, error::SerializeJsonError, web_api::VersionInfo}; +use miette::{IntoDiagnostic, Result}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Versions { @@ -54,9 +55,11 @@ impl Versions { } } - pub(crate) async fn save(&self) -> LibResult<()> { - let json_string = serde_json::to_string(&self)?; - fs::write(&*VERSION_FILE_PATH, json_string).await?; + pub(crate) async fn save(&self) -> Result<()> { + let json_string = serde_json::to_string(&self).map_err(SerializeJsonError::from)?; + fs::write(&*VERSION_FILE_PATH, json_string) + .await + .into_diagnostic()?; Ok(()) } diff --git a/src/web_api/error.rs b/src/web_api/error.rs deleted file mode 100644 index 8ee272c..0000000 --- a/src/web_api/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::io; - -use miette::Diagnostic; -use thiserror::Error; - -pub type ApiResult = Result; - -#[derive(Debug, Error, Diagnostic)] -pub enum ApiError { - #[error(transparent)] - Reqwest(#[from] reqwest::Error), - - #[error(transparent)] - Io(#[from] io::Error), - - #[error("{0}")] - Other(#[help] String), -} - -impl ApiError { - pub fn other(error: S) -> Self { - Self::Other(error.to_string()) - } -} diff --git a/src/web_api/mod.rs b/src/web_api/mod.rs index dcb2d4e..dc4c0c2 100644 --- a/src/web_api/mod.rs +++ b/src/web_api/mod.rs @@ -5,16 +5,15 @@ use std::{ use crate::{ consts::{NODE_ARCHIVE_SUFFIX, NODE_DIST_URL}, + error::ReqwestError, utils::progress_bar, }; -use self::error::{ApiError, ApiResult}; - use reqwest::Client; -pub mod error; mod model; use futures_util::StreamExt; +use miette::{miette, Context, IntoDiagnostic, Result}; pub use model::*; use tokio::io::{AsyncWrite, AsyncWriteExt}; @@ -44,14 +43,18 @@ impl WebApi { /// Returns the list of available node versions #[tracing::instrument(level = "trace")] - pub async fn get_versions(&self) -> ApiResult> { + pub async fn get_versions(&self) -> Result> { let versions = self .client .get(format!("{}/index.json", self.base_url)) .send() - .await? + .await + .map_err(ReqwestError::from) + .context("fetching versions")? .json() - .await?; + .await + .map_err(ReqwestError::from) + .context("fetching versions")?; Ok(versions) } @@ -63,7 +66,7 @@ impl WebApi { &self, version: S, writer: &mut W, - ) -> ApiResult { + ) -> Result { let res = self .client .get(format!( @@ -71,23 +74,31 @@ impl WebApi { self.base_url, *NODE_ARCHIVE_SUFFIX )) .send() - .await?; + .await + .map_err(ReqwestError::from) + .context("downloading nodejs")?; + let total_size = res .content_length() - .ok_or_else(|| ApiError::other("Missing content length"))?; + .ok_or_else(|| miette!("missing content_length header"))?; + let pb = progress_bar(total_size); pb.set_message(format!("Downloading node v{version}")); let mut stream = res.bytes_stream(); let mut total_downloaded = 0; while let Some(item) = stream.next().await { - let chunk = item?; - writer.write_all(&chunk).await?; + let chunk = item.map_err(ReqwestError::from)?; + writer + .write_all(&chunk) + .await + .into_diagnostic() + .context("writing download chunk to file")?; total_downloaded = min(chunk.len() as u64 + total_downloaded, total_size); pb.set_position(total_downloaded); } - writer.flush().await?; + writer.flush().await.into_diagnostic()?; pb.finish_with_message(format!("Downloaded node v{version}.")); Ok(total_downloaded)