use std::mem;
use anyhow::{anyhow, ensure, Result};
use camino::{Utf8Path, Utf8PathBuf};
use toml_edit::{value, Document, Entry, InlineTable, Item};
use url::Url;
use crate::core::{GitReference, PackageName};
use crate::internal::fsx;
use crate::internal::fsx::PathBufUtf8Ext;
use crate::sources::canonical_url::CanonicalUrl;
use super::tomlx::get_table_mut;
use super::{DepId, Op, OpCtx};
#[derive(Clone, Debug, Default)]
pub struct AddDependency {
pub dep: DepId,
pub path: Option<Utf8PathBuf>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
}
struct Dep {
name: PackageName,
source: Box<dyn Source>,
}
trait Source {
fn insert(self: Box<Self>, tab: &mut InlineTable);
}
struct RegistrySource {
version: String,
}
struct PathSource {
version: Option<String>,
path: String,
}
struct GitSource {
version: Option<String>,
git: String,
reference: GitReference,
}
impl Op for AddDependency {
#[tracing::instrument(level = "trace", skip(doc, ctx))]
fn apply_to(self: Box<Self>, doc: &mut Document, ctx: OpCtx<'_>) -> Result<()> {
let tab = get_table_mut(doc, &["dependencies"])?;
let dep = Dep::resolve(*self, ctx)?;
let was_sorted = {
let values = tab.as_table_like().unwrap().get_values();
is_sorted(values.iter().map(|(key, _)| key[0]))
};
let dep_key = dep.toml_key().to_string();
dep.upsert(tab.as_table_like_mut().unwrap().entry(&dep_key));
if was_sorted {
if let Some(table) = tab.as_table_like_mut() {
table.sort_values();
}
}
if let Some(t) = tab.as_inline_table_mut() {
t.fmt()
}
Ok(())
}
}
impl Dep {
fn resolve(op: AddDependency, ctx: OpCtx<'_>) -> Result<Self> {
use GitReference::*;
let name = op
.dep
.name
.ok_or_else(|| anyhow!("please specify package name"))?;
ensure!(
!(op.path.is_some() && op.git.is_some()),
"dependency ({name}) specification is ambiguous, \
only one of `git` or `path` is allowed"
);
if op.branch.is_some() || op.tag.is_some() || op.rev.is_some() {
ensure!(
op.git.is_some(),
"dependency ({name}) is non-Git, but provides `branch`, `tag` or `rev`"
);
ensure!(
[&op.branch, &op.tag, &op.rev]
.iter()
.filter(|o| o.is_some())
.count()
<= 1,
"dependency ({name}) specification is ambiguous, \
only one of `branch`, `tag` or `rev` is allowed"
);
}
let version = op.dep.version_req.map(|v| {
v.to_string().trim_start_matches('^').to_string()
});
let source: Box<dyn Source> = if let Some(path) = op.path {
let path = fsx::canonicalize(path)?.try_into_utf8()?;
let path = path_value(ctx.manifest_path, &path);
Box::new(PathSource { version, path })
} else if let Some(git) = op.git {
let reference = if let Some(branch) = op.branch {
Branch(branch.into())
} else if let Some(tag) = op.tag {
Tag(tag.into())
} else if let Some(rev) = op.rev {
Rev(rev.into())
} else {
DefaultBranch
};
let git = match Url::parse(&git) {
Ok(url) => CanonicalUrl::new(&url)
.map(|git_url| git_url.as_str().to_string())
.unwrap_or(git),
Err(_) => git,
};
Box::new(GitSource {
version,
git,
reference,
})
} else {
Box::new(RegistrySource {
version: version.ok_or_else(|| {
anyhow!("please specify package version requirement, for example: {name}@1.0.0")
})?,
})
};
Ok(Dep { name, source })
}
fn toml_key(&self) -> &str {
self.name.as_str()
}
fn upsert(self, entry: Entry<'_>) {
let item = entry.or_insert(value(InlineTable::new()));
expand_version_shortcut(item);
purge_source(item.as_inline_table_mut().unwrap());
self.source.insert(item.as_inline_table_mut().unwrap());
simplify_to_version_shortcut_if_possible(item);
}
}
impl Source for RegistrySource {
fn insert(self: Box<Self>, tab: &mut InlineTable) {
tab.insert("version", self.version.into());
}
}
impl Source for PathSource {
fn insert(self: Box<Self>, tab: &mut InlineTable) {
if let Some(version) = self.version {
tab.insert("version", version.into());
}
tab.insert("path", self.path.into());
}
}
impl Source for GitSource {
fn insert(self: Box<Self>, tab: &mut InlineTable) {
if let Some(version) = self.version {
tab.insert("version", version.into());
}
tab.insert("git", self.git.into());
match self.reference {
GitReference::DefaultBranch => {}
GitReference::Branch(branch) => {
tab.insert("branch", branch.to_string().into());
}
GitReference::Tag(tag) => {
tab.insert("tag", tag.to_string().into());
}
GitReference::Rev(rev) => {
tab.insert("rev", rev.to_string().into());
}
}
}
}
fn expand_version_shortcut(item: &mut Item) {
if item.is_value() && !item.is_inline_table() {
let version = mem::replace(item, value(InlineTable::new()));
item["version"] = version;
}
}
fn simplify_to_version_shortcut_if_possible(item: &mut Item) {
let can_simplify = item
.as_table_like()
.map(|tab| tab.len() == 1 && tab.iter().next().unwrap().0 == "version")
.unwrap_or(false);
if can_simplify {
let version = item.as_table_like_mut().unwrap().remove("version").unwrap();
*item = version;
}
}
fn purge_source(tab: &mut InlineTable) {
tab.remove("path");
tab.remove("git");
tab.remove("branch");
tab.remove("tag");
tab.remove("rev");
}
fn path_value(manifest_path: &Utf8Path, abs_path: &Utf8Path) -> String {
let package_root = manifest_path
.parent()
.expect("Manifest path should point to manifest file.");
pathdiff::diff_utf8_paths(abs_path, package_root)
.expect("Both paths should be absolute at this point.")
.as_str()
.replace('\\', "/")
}
fn is_sorted(mut item: impl Iterator<Item = impl PartialOrd>) -> bool {
let mut last = match item.next() {
Some(e) => e,
None => return true,
};
for current in item {
if current < last {
return false;
}
last = current;
}
true
}