use std::{
collections::{HashMap, HashSet},
path::PathBuf,
process::Command,
};
use serde::Deserialize;
use super::package::Package;
use crate::{DEFAULT_REGISTRY, DEFAULT_REGISTRY_CRATE};
pub(super) struct Workspace {
packages: HashMap<String, MetadataPackage>,
direct_deps: HashSet<String>,
}
#[derive(Deserialize)]
struct Metadata {
packages: Vec<MetadataPackage>,
workspace_default_members: Vec<String>,
}
#[derive(Deserialize)]
struct MetadataPackage {
name: String,
id: String,
manifest_path: PathBuf,
dependencies: Vec<Dependency>,
#[serde(default)]
metadata: serde_json::Value,
}
#[derive(Deserialize)]
struct Dependency {
name: String,
}
impl Workspace {
pub(super) fn load(package: &Package) -> Result<Self, String> {
let output = Command::new("cargo")
.args(["metadata", "--format-version=1"])
.current_dir(package.root())
.output()
.map_err(|error| format!("failed to run `cargo metadata`: {error}"))?;
if !output.status.success() {
return Err(format!(
"`cargo metadata` failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
let metadata: Metadata = serde_json::from_slice(&output.stdout)
.map_err(|error| format!("failed to parse `cargo metadata` output: {error}"))?;
let members: HashSet<&str> = metadata
.workspace_default_members
.iter()
.map(String::as_str)
.collect();
let direct_deps = metadata
.packages
.iter()
.filter(|package| members.contains(package.id.as_str()))
.flat_map(|package| package.dependencies.iter().map(|dep| dep.name.clone()))
.collect();
let packages = metadata
.packages
.into_iter()
.map(|package| (package.name.clone(), package))
.collect();
Ok(Self {
packages,
direct_deps,
})
}
pub(super) fn registry_dir(&self, name: &str) -> Result<PathBuf, String> {
if name == DEFAULT_REGISTRY {
let package = self.packages.get(DEFAULT_REGISTRY_CRATE).ok_or_else(|| {
format!(
"the built-in registry crate `{DEFAULT_REGISTRY_CRATE}` is not in the \
dependency graph; enable the `ui` feature on `topcoat`"
)
})?;
return registry_path(package, DEFAULT_REGISTRY_CRATE);
}
let package = self.packages.get(name).ok_or_else(|| {
format!("registry crate `{name}` is not in the dependency graph; add it to Cargo.toml")
})?;
if !self.direct_deps.contains(name) {
return Err(format!(
"registry crate `{name}` must be a direct dependency in Cargo.toml"
));
}
registry_path(package, name)
}
pub(super) fn available_registries(&self) -> Vec<String> {
let mut names: Vec<String> = self
.packages
.values()
.filter(|package| {
package.name != DEFAULT_REGISTRY_CRATE
&& self.direct_deps.contains(&package.name)
&& registry_subdir(package).is_some()
})
.map(|package| package.name.clone())
.collect();
if self
.packages
.get(DEFAULT_REGISTRY_CRATE)
.is_some_and(|package| registry_subdir(package).is_some())
{
names.push(DEFAULT_REGISTRY.to_string());
}
names.sort();
names.dedup();
names
}
}
fn registry_path(package: &MetadataPackage, name: &str) -> Result<PathBuf, String> {
let registry = registry_subdir(package).ok_or_else(|| {
format!(
"crate `{name}` is not a topcoat-ui registry \
(missing `[package.metadata.topcoat-ui].registry`)"
)
})?;
let crate_root = package
.manifest_path
.parent()
.expect("a manifest path always has a parent directory");
Ok(crate_root.join(registry))
}
fn registry_subdir(package: &MetadataPackage) -> Option<&str> {
package
.metadata
.get("topcoat-ui")?
.get("registry")?
.as_str()
}