use std::path::PathBuf;
use serde::Deserialize;
use super::error::ReleaseError;
use super::metadata::{CrateMetadata, ReleaseMetadata};
use crate::process::{ProcessSpec, run_capture};
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct CargoMetadata {
pub packages: Vec<Package>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct Package {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(rename = "manifest_path")]
pub manifest_path: String,
pub publish: Option<Vec<String>>,
pub metadata: Option<serde_json::Value>,
pub targets: Vec<Target>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct Dependency {
pub name: String,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
#[allow(dead_code)]
pub optional: Option<bool>,
#[serde(default)]
pub req: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub rename: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct Target {
pub kind: Vec<String>,
}
fn is_publishable(publish: &Option<Vec<String>>) -> bool {
match publish {
None => true,
Some(registries) => registries.iter().any(|r| r == "crates-io"),
}
}
fn is_proc_macro(package: &Package) -> bool {
package
.targets
.iter()
.any(|t| t.kind.iter().any(|k| k == "proc-macro"))
}
#[derive(Debug, Clone)]
pub(crate) struct DiscoveredCrate {
pub name: String,
#[allow(dead_code)]
pub manifest_path: PathBuf,
#[allow(dead_code)]
pub proc_macro: bool,
pub publishable: bool,
pub metadata: CrateMetadata,
}
pub(crate) fn load_metadata() -> Result<CargoMetadata, ReleaseError> {
let spec = ProcessSpec::new("cargo", std::env::current_dir().unwrap_or_default())
.arg("metadata")
.arg("--no-deps")
.arg("--format-version")
.arg("1");
let json = run_capture(&spec)?;
let metadata: CargoMetadata = serde_json::from_slice(&json)?;
Ok(metadata)
}
pub(crate) fn discover(document: &CargoMetadata) -> Vec<DiscoveredCrate> {
document
.packages
.iter()
.map(|package| {
let metadata = match package.metadata.as_ref().and_then(|m| m.get("arcature")) {
None => CrateMetadata::Absent,
Some(value) => match serde_json::from_value::<ReleaseMetadata>(value.clone()) {
Ok(parsed) => CrateMetadata::Valid(parsed),
Err(error) => CrateMetadata::Malformed(error.to_string()),
},
};
DiscoveredCrate {
name: package.name.clone(),
manifest_path: PathBuf::from(&package.manifest_path),
publishable: is_publishable(&package.publish),
proc_macro: is_proc_macro(package),
metadata,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::release::metadata::Role;
fn package(name: &str, publish: Option<&[&str]>, metadata: Option<&str>) -> Package {
Package {
name: name.to_string(),
version: None,
manifest_path: format!("/repo/crates/{name}/Cargo.toml"),
publish: publish.map(|p| p.iter().map(|s| (*s).to_string()).collect()),
metadata: metadata.map(|m| serde_json::from_str(m).unwrap()),
targets: vec![Target {
kind: vec!["lib".to_string()],
}],
dependencies: Vec::new(),
}
}
#[test]
fn unrestricted_publish_is_publishable() {
assert!(is_publishable(&None));
}
#[test]
fn empty_publish_list_is_not_publishable() {
assert!(!is_publishable(&Some(vec![])));
}
#[test]
fn crates_io_registry_is_publishable() {
assert!(is_publishable(&Some(vec!["crates-io".to_string()])));
assert!(!is_publishable(&Some(vec!["private".to_string()])));
}
#[test]
fn absent_metadata_yields_absent() {
let doc = CargoMetadata {
packages: vec![package("arcature-auth", None, None)],
};
let crates = discover(&doc);
assert_eq!(crates.len(), 1);
assert!(matches!(crates[0].metadata, CrateMetadata::Absent));
assert!(crates[0].publishable);
}
#[test]
fn valid_metadata_parses() {
let doc = CargoMetadata {
packages: vec![package(
"arcature-auth",
None,
Some(
r#"{"arcature":{"publish":true,"role":"subsystem","release-unit":"arcature-auth"}}"#,
),
)],
};
let crates = discover(&doc);
match &crates[0].metadata {
CrateMetadata::Valid(md) => {
assert!(md.publish);
assert_eq!(md.role, Some(Role::Subsystem));
assert_eq!(md.release_unit.as_deref(), Some("arcature-auth"));
}
other => panic!("expected Valid, got {other:?}"),
}
}
#[test]
fn malformed_metadata_is_captured_not_fatal() {
let doc = CargoMetadata {
packages: vec![package(
"arcature-bad",
None,
Some(r#"{"arcature":{"publish":true,"role":"glue","release-unit":"x"}}"#),
)],
};
let crates = discover(&doc);
assert!(matches!(crates[0].metadata, CrateMetadata::Malformed(_)));
}
#[test]
fn proc_macro_target_detected() {
let mut pkg = package("arcature-dx", None, None);
pkg.targets = vec![Target {
kind: vec!["proc-macro".to_string()],
}];
assert!(is_proc_macro(&pkg));
let lib = package("arcature", None, None);
assert!(!is_proc_macro(&lib));
}
}