use std::{collections::HashMap, ffi::OsStr, path::Path};
use anyhow::{format_err, Context as _, Result};
use camino::Utf8PathBuf;
use serde_json::{Map, Value};
type Object = Map<String, Value>;
type ParseResult<T> = Result<T, &'static str>;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct PackageId {
repr: String,
}
impl From<String> for PackageId {
fn from(repr: String) -> Self {
Self { repr }
}
}
pub(crate) struct Metadata {
pub(crate) packages: HashMap<PackageId, Package>,
pub(crate) workspace_members: Vec<PackageId>,
pub(crate) workspace_root: Utf8PathBuf,
pub(crate) target_directory: Utf8PathBuf,
}
impl Metadata {
pub(crate) fn new(manifest_path: &Path, cargo: &OsStr) -> Result<Self> {
let mut cmd = cmd!(
cargo,
"metadata",
"--format-version=1",
"--no-deps",
"--manifest-path",
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).map_err(|s| format_err!("failed to parse `{s}` field from metadata"))
}
fn from_obj(mut map: Object) -> ParseResult<Self> {
let workspace_members: Vec<_> = map
.remove_array("workspace_members")?
.into_iter()
.map(|v| into_string(v).ok_or("workspace_members"))
.collect::<Result<_, _>>()?;
Ok(Self {
packages: map
.remove_array("packages")?
.into_iter()
.map(Package::from_value)
.collect::<Result<_, _>>()?,
workspace_members,
workspace_root: map.remove_string("workspace_root")?,
target_directory: map.remove_string("target_directory")?,
})
}
}
pub(crate) struct Package {
pub(crate) name: String,
pub(crate) targets: Vec<Target>,
pub(crate) manifest_path: Utf8PathBuf,
}
impl Package {
fn from_value(mut value: Value) -> ParseResult<(PackageId, Self)> {
let map = value.as_object_mut().ok_or("packages")?;
let id = map.remove_string("id")?;
Ok((id, Self {
name: map.remove_string("name")?,
targets: map
.remove_array("targets")?
.into_iter()
.map(Target::from_value)
.collect::<Result<_, _>>()?,
manifest_path: map.remove_string("manifest_path")?,
}))
}
}
pub(crate) struct Target {
pub(crate) name: String,
}
impl Target {
fn from_value(mut value: Value) -> ParseResult<Self> {
let map = value.as_object_mut().ok_or("targets")?;
Ok(Self { name: map.remove_string("name")? })
}
}
#[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
}
}
fn into_object(value: Value) -> Option<Object> {
if let Value::Object(object) = value {
Some(object)
} 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_object(&mut self, key: &'static str) -> ParseResult<Object>;
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_object(&mut self, key: &'static str) -> ParseResult<Object> {
self.remove(key).and_then(into_object).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)
}
}