use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use serde::Deserialize;
use toml_edit::{Array, Item, Table, Value, value};
use uv_distribution_types::Requirement;
use uv_fs::{PortablePath, Simplified};
use uv_normalize::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::PythonRequest;
use uv_settings::ToolOptions;
#[derive(Debug, Clone, Deserialize)]
#[serde(try_from = "ToolWire", into = "ToolWire")]
pub struct Tool {
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
excludes: Vec<PackageName>,
build_constraints: Vec<Requirement>,
python: Option<PythonRequest>,
entrypoints: Vec<ToolEntrypoint>,
options: ToolOptions,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct ToolWire {
#[serde(default)]
requirements: Vec<RequirementWire>,
#[serde(default)]
constraints: Vec<Requirement>,
#[serde(default)]
overrides: Vec<Requirement>,
#[serde(default)]
excludes: Vec<PackageName>,
#[serde(default)]
build_constraint_dependencies: Vec<Requirement>,
python: Option<PythonRequest>,
entrypoints: Vec<ToolEntrypoint>,
#[serde(default)]
options: ToolOptions,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
enum RequirementWire {
Requirement(Requirement),
Deprecated(uv_pep508::Requirement<VerbatimParsedUrl>),
}
impl From<Tool> for ToolWire {
fn from(tool: Tool) -> Self {
Self {
requirements: tool
.requirements
.into_iter()
.map(RequirementWire::Requirement)
.collect(),
constraints: tool.constraints,
overrides: tool.overrides,
excludes: tool.excludes,
build_constraint_dependencies: tool.build_constraints,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
}
}
}
impl TryFrom<ToolWire> for Tool {
type Error = serde::de::value::Error;
fn try_from(tool: ToolWire) -> Result<Self, Self::Error> {
Ok(Self {
requirements: tool
.requirements
.into_iter()
.map(|req| match req {
RequirementWire::Requirement(requirements) => requirements,
RequirementWire::Deprecated(requirement) => Requirement::from(requirement),
})
.collect(),
constraints: tool.constraints,
overrides: tool.overrides,
excludes: tool.excludes,
build_constraints: tool.build_constraint_dependencies,
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ToolEntrypoint {
pub name: String,
pub install_path: PathBuf,
pub from: Option<String>,
}
impl Display for ToolEntrypoint {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[cfg(windows)]
{
write!(
f,
"{} ({})",
self.name,
self.install_path
.simplified_display()
.to_string()
.replace('/', "\\")
)
}
#[cfg(unix)]
{
write!(
f,
"{} ({})",
self.name,
self.install_path.simplified_display()
)
}
}
}
fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
let mut array = elements
.map(Into::into)
.map(|mut value| {
value.decor_mut().set_prefix("\n ");
value
})
.collect::<Array>();
array.set_trailing_comma(true);
array.set_trailing("\n");
array
}
impl Tool {
pub fn new(
requirements: Vec<Requirement>,
constraints: Vec<Requirement>,
overrides: Vec<Requirement>,
excludes: Vec<PackageName>,
build_constraints: Vec<Requirement>,
python: Option<PythonRequest>,
entrypoints: impl IntoIterator<Item = ToolEntrypoint>,
options: ToolOptions,
) -> Self {
let mut entrypoints: Vec<_> = entrypoints.into_iter().collect();
entrypoints.sort();
Self {
requirements,
constraints,
overrides,
excludes,
build_constraints,
python,
entrypoints,
options,
}
}
#[must_use]
pub fn with_options(self, options: ToolOptions) -> Self {
Self { options, ..self }
}
pub(crate) fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
let mut table = Table::new();
if !self.requirements.is_empty() {
table.insert("requirements", {
let requirements = self
.requirements
.iter()
.map(|requirement| {
serde::Serialize::serialize(
&requirement,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
let requirements = match requirements.as_slice() {
[] => Array::new(),
[requirement] => Array::from_iter([requirement]),
requirements => each_element_on_its_line_array(requirements.iter()),
};
value(requirements)
});
}
if !self.constraints.is_empty() {
table.insert("constraints", {
let constraints = self
.constraints
.iter()
.map(|constraint| {
serde::Serialize::serialize(
&constraint,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
let constraints = match constraints.as_slice() {
[] => Array::new(),
[constraint] => Array::from_iter([constraint]),
constraints => each_element_on_its_line_array(constraints.iter()),
};
value(constraints)
});
}
if !self.overrides.is_empty() {
table.insert("overrides", {
let overrides = self
.overrides
.iter()
.map(|r#override| {
serde::Serialize::serialize(
&r#override,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
let overrides = match overrides.as_slice() {
[] => Array::new(),
[r#override] => Array::from_iter([r#override]),
overrides => each_element_on_its_line_array(overrides.iter()),
};
value(overrides)
});
}
if !self.excludes.is_empty() {
table.insert("excludes", {
let excludes = self
.excludes
.iter()
.map(|r#exclude| {
serde::Serialize::serialize(
&r#exclude,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
let excludes = match excludes.as_slice() {
[] => Array::new(),
[r#exclude] => Array::from_iter([r#exclude]),
excludes => each_element_on_its_line_array(excludes.iter()),
};
value(excludes)
});
}
if !self.build_constraints.is_empty() {
table.insert("build-constraint-dependencies", {
let build_constraints = self
.build_constraints
.iter()
.map(|r#build_constraint| {
serde::Serialize::serialize(
&r#build_constraint,
toml_edit::ser::ValueSerializer::new(),
)
})
.collect::<Result<Vec<_>, _>>()?;
let build_constraints = match build_constraints.as_slice() {
[] => Array::new(),
[r#build_constraint] => Array::from_iter([r#build_constraint]),
build_constraints => each_element_on_its_line_array(build_constraints.iter()),
};
value(build_constraints)
});
}
if let Some(ref python) = self.python {
table.insert(
"python",
value(serde::Serialize::serialize(
&python,
toml_edit::ser::ValueSerializer::new(),
)?),
);
}
table.insert("entrypoints", {
let entrypoints = each_element_on_its_line_array(
self.entrypoints
.iter()
.map(ToolEntrypoint::to_toml)
.map(Table::into_inline_table),
);
value(entrypoints)
});
if self.options != ToolOptions::default() {
let serialized =
serde::Serialize::serialize(&self.options, toml_edit::ser::ValueSerializer::new())?;
let Value::InlineTable(serialized) = serialized else {
return Err(toml_edit::ser::Error::Custom(
"Expected an inline table".to_string(),
));
};
table.insert("options", Item::Table(serialized.into_table()));
}
Ok(table)
}
pub fn entrypoints(&self) -> &[ToolEntrypoint] {
&self.entrypoints
}
pub fn requirements(&self) -> &[Requirement] {
&self.requirements
}
pub fn constraints(&self) -> &[Requirement] {
&self.constraints
}
pub fn overrides(&self) -> &[Requirement] {
&self.overrides
}
pub fn excludes(&self) -> &[PackageName] {
&self.excludes
}
pub fn build_constraints(&self) -> &[Requirement] {
&self.build_constraints
}
pub fn python(&self) -> &Option<PythonRequest> {
&self.python
}
pub fn options(&self) -> &ToolOptions {
&self.options
}
}
impl ToolEntrypoint {
pub fn new(name: &str, install_path: PathBuf, from: String) -> Self {
let name = name
.trim_end_matches(std::env::consts::EXE_SUFFIX)
.to_string();
Self {
name,
install_path,
from: Some(from),
}
}
pub(crate) fn to_toml(&self) -> Table {
let mut table = Table::new();
table.insert("name", value(&self.name));
table.insert(
"install-path",
value(PortablePath::from(&self.install_path).to_string()),
);
if let Some(from) = &self.from {
table.insert("from", value(from));
}
table
}
}