use std::fmt::{Display, Formatter};
use std::fs;
use std::path::PathBuf;
use color_eyre::eyre::Result;
use versions::{Chunk, Version};
use crate::config::Config;
use crate::dirs;
use crate::hash::hash_to_str;
use crate::plugins::PluginName;
use crate::tool::Tool;
use crate::toolset::{ToolVersionOptions, ToolVersionRequest};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ToolVersion {
pub request: ToolVersionRequest,
pub plugin_name: PluginName,
pub version: String,
pub opts: ToolVersionOptions,
}
impl ToolVersion {
pub fn new(
tool: &Tool,
request: ToolVersionRequest,
opts: ToolVersionOptions,
version: String,
) -> Self {
ToolVersion {
plugin_name: tool.name.to_string(),
version,
request,
opts,
}
}
pub fn resolve(
config: &Config,
tool: &Tool,
request: ToolVersionRequest,
opts: ToolVersionOptions,
latest_versions: bool,
) -> Result<Self> {
if !tool.is_installed() {
let tv = Self::new(tool, request.clone(), opts.clone(), request.version());
return Ok(tv);
}
let tv = match request.clone() {
ToolVersionRequest::Version(_, v) => {
Self::resolve_version(config, tool, request, latest_versions, &v, opts)?
}
ToolVersionRequest::Prefix(_, prefix) => {
Self::resolve_prefix(config, tool, request, &prefix, opts)?
}
ToolVersionRequest::Sub {
sub, orig_version, ..
} => Self::resolve_sub(
config,
tool,
request,
latest_versions,
&sub,
&orig_version,
opts,
)?,
_ => {
let version = request.version();
Self::new(tool, request, opts, version)
}
};
Ok(tv)
}
pub fn install_path(&self) -> PathBuf {
let pathname = match &self.request {
ToolVersionRequest::Path(_, p) => p.to_string_lossy().to_string(),
_ => self.tv_pathname(),
};
dirs::INSTALLS.join(&self.plugin_name).join(pathname)
}
pub fn cache_path(&self) -> PathBuf {
dirs::CACHE.join(&self.plugin_name).join(self.tv_pathname())
}
pub fn download_path(&self) -> PathBuf {
dirs::DOWNLOADS
.join(&self.plugin_name)
.join(self.tv_pathname())
}
pub fn latest_version(&self, config: &Config, tool: &Tool) -> Result<String> {
let tv = self
.request
.resolve(config, tool, self.opts.clone(), true)?;
Ok(tv.version)
}
fn tv_pathname(&self) -> String {
match &self.request {
ToolVersionRequest::Version(_, _) => self.version.to_string(),
ToolVersionRequest::Prefix(_, _) => self.version.to_string(),
ToolVersionRequest::Sub { .. } => self.version.to_string(),
ToolVersionRequest::Ref(_, r) => format!("ref-{}", r),
ToolVersionRequest::Path(_, p) => format!("path-{}", hash_to_str(p)),
ToolVersionRequest::System(_) => "system".to_string(),
}
}
fn resolve_version(
config: &Config,
tool: &Tool,
request: ToolVersionRequest,
latest_versions: bool,
v: &str,
opts: ToolVersionOptions,
) -> Result<ToolVersion> {
let v = config.resolve_alias(&tool.name, v)?;
match v.split_once(':') {
Some(("ref", r)) => {
return Ok(Self::resolve_ref(tool, r.to_string(), opts));
}
Some(("path", p)) => {
return Self::resolve_path(tool, PathBuf::from(p), opts);
}
Some(("prefix", p)) => {
return Self::resolve_prefix(config, tool, request, p, opts);
}
Some((part, v)) if part.starts_with("sub-") => {
let sub = part.split_once('-').unwrap().1;
return Self::resolve_sub(config, tool, request, latest_versions, sub, v, opts);
}
_ => (),
}
let build = |v| Ok(Self::new(tool, request.clone(), opts.clone(), v));
if !tool.is_installed() {
return build(v);
}
let existing = build(v.clone())?;
if tool.is_version_installed(&existing) {
return Ok(existing);
}
if v == "latest" {
if !latest_versions {
if let Some(v) = tool.latest_installed_version(None)? {
return build(v);
}
}
if let Some(v) = tool.latest_version(&config.settings, None)? {
return build(v);
}
}
if !latest_versions {
let matches = tool.list_installed_versions_matching(&v)?;
if matches.contains(&v) {
return build(v);
}
if let Some(v) = matches.last() {
return build(v.clone());
}
}
let matches = tool.list_versions_matching(&config.settings, &v)?;
if matches.contains(&v) {
return build(v);
}
Self::resolve_prefix(config, tool, request, &v, opts)
}
fn resolve_sub(
config: &Config,
tool: &Tool,
request: ToolVersionRequest,
latest_versions: bool,
sub: &str,
v: &str,
opts: ToolVersionOptions,
) -> Result<Self> {
let v = match v {
"latest" => tool.latest_version(&config.settings, None)?.unwrap(),
_ => config.resolve_alias(&tool.name, v)?,
};
let v = version_sub(&v, sub);
Self::resolve_version(config, tool, request, latest_versions, &v, opts)
}
fn resolve_prefix(
config: &Config,
tool: &Tool,
request: ToolVersionRequest,
prefix: &str,
opts: ToolVersionOptions,
) -> Result<Self> {
let matches = tool.list_versions_matching(&config.settings, prefix)?;
let v = match matches.last() {
Some(v) => v,
None => prefix,
};
Ok(Self::new(tool, request, opts, v.to_string()))
}
fn resolve_ref(tool: &Tool, r: String, opts: ToolVersionOptions) -> Self {
let request = ToolVersionRequest::Ref(tool.name.clone(), r);
let version = request.version();
Self::new(tool, request, opts, version)
}
fn resolve_path(tool: &Tool, path: PathBuf, opts: ToolVersionOptions) -> Result<ToolVersion> {
let path = fs::canonicalize(path)?;
let request = ToolVersionRequest::Path(tool.name.clone(), path);
let version = request.version();
Ok(Self::new(tool, request, opts, version))
}
}
impl Display for ToolVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}@{}", &self.plugin_name, &self.version)
}
}
fn version_sub(orig: &str, sub: &str) -> String {
let mut orig = Version::new(orig).unwrap();
let sub = Version::new(sub).unwrap();
while orig.chunks.0.len() > sub.chunks.0.len() {
orig.chunks.0.pop();
}
for (i, orig_chunk) in orig.clone().chunks.0.iter().enumerate() {
let m = sub.nth(i).unwrap();
orig.chunks.0[i] = Chunk::Numeric(orig_chunk.single_digit().unwrap() - m);
}
orig.to_string()
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_str_eq;
use super::*;
#[test]
fn test_version_sub() {
assert_str_eq!(version_sub("18.2.3", "2"), "16");
assert_str_eq!(version_sub("18.2.3", "0.1"), "18.1");
}
}