use std::{
collections::HashMap,
ffi::OsStr,
ops,
path::{Path, PathBuf},
};
use anyhow::{Context as _, Result, format_err};
use serde_json::{Map, Value};
type Object = Map<String, Value>;
type ParseResult<T> = Result<T, &'static str>;
#[derive(Clone, Copy)]
#[repr(transparent)]
pub(crate) struct PackageId {
index: usize,
}
pub(crate) struct Metadata {
pub(crate) cargo_version: u32,
pub(crate) packages: Box<[Package]>,
pub(crate) workspace_members: Box<[PackageId]>,
pub(crate) workspace_root: PathBuf,
}
impl Metadata {
pub(crate) fn new(
manifest_path: Option<&str>,
cargo: &OsStr,
cargo_version: u32,
) -> Result<Self> {
let mut cmd = cmd!(cargo, "metadata", "--format-version=1", "--no-deps");
if let Some(manifest_path) = manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
}
let json = cmd.read()?;
let map = serde_json::from_str(&json)
.with_context(|| format!("failed to parse output from {cmd}"))?;
Self::from_obj(map, cargo_version)
.map_err(|s| format_err!("failed to parse `{s}` field from metadata"))
}
fn from_obj(mut map: Object, cargo_version: u32) -> ParseResult<Self> {
let raw_packages = map.remove_array("packages")?;
let mut packages = Vec::with_capacity(raw_packages.len());
let mut pkg_id_map = HashMap::with_capacity(raw_packages.len());
for (i, pkg) in raw_packages.into_iter().enumerate() {
let (id, pkg) = Package::from_value(pkg, cargo_version)?;
pkg_id_map.insert(id, i);
packages.push(pkg);
}
let workspace_members = map
.remove_array("workspace_members")?
.into_iter()
.map(|v| -> ParseResult<_> {
let id: String = into_string(v).ok_or("workspace_members")?;
Ok(PackageId { index: pkg_id_map[&id] })
})
.collect::<Result<_, _>>()?;
Ok(Self {
cargo_version,
packages: packages.into_boxed_slice(),
workspace_members,
workspace_root: map.remove_string("workspace_root")?,
})
}
}
impl ops::Index<PackageId> for Metadata {
type Output = Package;
#[inline]
fn index(&self, index: PackageId) -> &Self::Output {
&self.packages[index.index]
}
}
pub(crate) struct Package {
pub(crate) manifest_path: Box<Path>,
pub(crate) publish: bool,
}
impl Package {
fn from_value(mut value: Value, cargo_version: u32) -> ParseResult<(String, Self)> {
let map = value.as_object_mut().ok_or("packages")?;
let id = map.remove_string("id")?;
Ok((id, Self {
manifest_path: map.remove_string::<PathBuf>("manifest_path")?.into_boxed_path(),
publish: if cargo_version >= 39 {
map.remove_nullable("publish", into_array)?.is_none_or(|a| !a.is_empty())
} else {
true
},
}))
}
}
#[allow(clippy::option_option)]
fn allow_null<T>(value: Value, f: impl FnOnce(Value) -> Option<T>) -> Option<Option<T>> {
if value.is_null() { Some(None) } else { f(value).map(Some) }
}
fn into_string<S: From<String>>(value: Value) -> Option<S> {
if let Value::String(string) = value { Some(string.into()) } else { None }
}
fn into_array(value: Value) -> Option<Vec<Value>> {
if let Value::Array(array) = value { Some(array) } else { None }
}
trait ObjectExt {
fn remove_string<S: From<String>>(&mut self, key: &'static str) -> ParseResult<S>;
fn remove_array(&mut self, key: &'static str) -> ParseResult<Vec<Value>>;
fn remove_nullable<T>(
&mut self,
key: &'static str,
f: impl FnOnce(Value) -> Option<T>,
) -> ParseResult<Option<T>>;
}
impl ObjectExt for Object {
fn remove_string<S: From<String>>(&mut self, key: &'static str) -> ParseResult<S> {
self.remove(key).and_then(into_string).ok_or(key)
}
fn remove_array(&mut self, key: &'static str) -> ParseResult<Vec<Value>> {
self.remove(key).and_then(into_array).ok_or(key)
}
fn remove_nullable<T>(
&mut self,
key: &'static str,
f: impl FnOnce(Value) -> Option<T>,
) -> ParseResult<Option<T>> {
self.remove(key).and_then(|v| allow_null(v, f)).ok_or(key)
}
}