use std::path::Path;
use toml::Value;
use super::MetadataConfig;
pub fn derive_metadata_from_cargo_toml(crate_dir: &Path) -> MetadataConfig {
let cargo_toml = crate_dir.join("Cargo.toml");
let Ok(content) = std::fs::read_to_string(&cargo_toml) else {
return MetadataConfig::default();
};
let Ok(doc) = content.parse::<Value>() else {
return MetadataConfig::default();
};
let workspace_pkg = WorkspacePackage::resolve(crate_dir);
let Some(package) = doc.get("package").and_then(Value::as_table) else {
return MetadataConfig::default();
};
let description = string_field(package, &workspace_pkg, "description");
let license = string_field(package, &workspace_pkg, "license");
let repository = string_field(package, &workspace_pkg, "repository");
let homepage = string_field(package, &workspace_pkg, "homepage").or_else(|| repository.clone());
let documentation = string_field(package, &workspace_pkg, "documentation");
let maintainers = string_array_field(package, &workspace_pkg, "authors");
MetadataConfig {
description,
homepage,
documentation,
license,
repository,
maintainers,
mod_timestamp: None,
full_description: None,
commit_author: None,
}
}
type Table = toml::map::Map<String, Value>;
struct WorkspacePackage {
table: Option<Table>,
}
impl WorkspacePackage {
fn resolve(crate_dir: &Path) -> Self {
let mut dir = Some(crate_dir);
while let Some(d) = dir {
let candidate = d.join("Cargo.toml");
if let Ok(content) = std::fs::read_to_string(&candidate)
&& let Ok(doc) = content.parse::<Value>()
&& let Some(ws) = doc.get("workspace").and_then(Value::as_table)
{
let table = ws.get("package").and_then(Value::as_table).cloned();
return WorkspacePackage { table };
}
dir = d.parent();
}
WorkspacePackage { table: None }
}
fn string(&self, key: &str) -> Option<String> {
non_empty_string(self.table.as_ref()?.get(key)?)
}
fn string_array(&self, key: &str) -> Option<Vec<String>> {
string_array_from_value(self.table.as_ref()?.get(key)?)
}
}
fn string_field(package: &Table, workspace: &WorkspacePackage, key: &str) -> Option<String> {
match package.get(key) {
Some(item) if is_workspace_inherited(item) => workspace.string(key),
Some(item) => non_empty_string(item),
None => None,
}
}
fn string_array_field(
package: &Table,
workspace: &WorkspacePackage,
key: &str,
) -> Option<Vec<String>> {
match package.get(key) {
Some(item) if is_workspace_inherited(item) => workspace.string_array(key),
Some(item) => string_array_from_value(item),
None => None,
}
}
fn non_empty_string(item: &Value) -> Option<String> {
item.as_str().map(str::to_string).filter(|s| !s.is_empty())
}
fn string_array_from_value(item: &Value) -> Option<Vec<String>> {
let arr = item.as_array()?;
let values: Vec<String> = arr
.iter()
.filter_map(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if values.is_empty() {
None
} else {
Some(values)
}
}
fn is_workspace_inherited(item: &Value) -> bool {
item.as_table()
.and_then(|t| t.get("workspace"))
.and_then(Value::as_bool)
== Some(true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn write(dir: &Path, name: &str, body: &str) {
fs::write(dir.join(name), body).unwrap();
}
#[test]
fn derives_all_fields_from_package_table() {
let dir = tempdir().unwrap();
write(
dir.path(),
"Cargo.toml",
r#"
[package]
name = "demo"
description = "a demo crate"
license = "MIT"
homepage = "https://demo.example"
documentation = "https://docs.rs/demo"
repository = "https://github.com/acme/demo"
authors = ["Ada <ada@example.com>", "Grace"]
"#,
);
let m = derive_metadata_from_cargo_toml(dir.path());
assert_eq!(m.description.as_deref(), Some("a demo crate"));
assert_eq!(m.license.as_deref(), Some("MIT"));
assert_eq!(m.homepage.as_deref(), Some("https://demo.example"));
assert_eq!(m.documentation.as_deref(), Some("https://docs.rs/demo"));
assert_eq!(
m.maintainers,
Some(vec![
"Ada <ada@example.com>".to_string(),
"Grace".to_string()
])
);
}
#[test]
fn homepage_falls_back_to_repository() {
let dir = tempdir().unwrap();
write(
dir.path(),
"Cargo.toml",
r#"
[package]
name = "demo"
repository = "https://github.com/acme/demo"
"#,
);
let m = derive_metadata_from_cargo_toml(dir.path());
assert_eq!(m.homepage.as_deref(), Some("https://github.com/acme/demo"));
}
#[test]
fn license_file_only_does_not_fabricate_license() {
let dir = tempdir().unwrap();
write(
dir.path(),
"Cargo.toml",
r#"
[package]
name = "demo"
license-file = "LICENSE.txt"
"#,
);
let m = derive_metadata_from_cargo_toml(dir.path());
assert!(
m.license.is_none(),
"license-file must not synthesise an SPDX id"
);
}
#[test]
fn resolves_workspace_inherited_fields() {
let root = tempdir().unwrap();
write(
root.path(),
"Cargo.toml",
r#"
[workspace]
members = ["crates/demo"]
[workspace.package]
license = "Apache-2.0"
homepage = "https://ws.example"
authors = ["Workspace Author <ws@example.com>"]
"#,
);
let crate_dir = root.path().join("crates/demo");
fs::create_dir_all(&crate_dir).unwrap();
write(
&crate_dir,
"Cargo.toml",
r#"
[package]
name = "demo"
description = "inherits the rest"
license.workspace = true
homepage.workspace = true
authors.workspace = true
"#,
);
let m = derive_metadata_from_cargo_toml(&crate_dir);
assert_eq!(m.description.as_deref(), Some("inherits the rest"));
assert_eq!(m.license.as_deref(), Some("Apache-2.0"));
assert_eq!(m.homepage.as_deref(), Some("https://ws.example"));
assert_eq!(
m.maintainers,
Some(vec!["Workspace Author <ws@example.com>".to_string()])
);
}
#[test]
fn missing_cargo_toml_yields_empty_metadata() {
let dir = tempdir().unwrap();
let m = derive_metadata_from_cargo_toml(dir.path());
assert!(m.description.is_none());
assert!(m.license.is_none());
assert!(m.homepage.is_none());
assert!(m.maintainers.is_none());
}
#[test]
fn empty_strings_collapse_to_none() {
let dir = tempdir().unwrap();
write(
dir.path(),
"Cargo.toml",
r#"
[package]
name = "demo"
description = ""
license = ""
authors = []
"#,
);
let m = derive_metadata_from_cargo_toml(dir.path());
assert!(m.description.is_none());
assert!(m.license.is_none());
assert!(m.maintainers.is_none());
}
}