use crate::core::config::{CargoPackageFilesConfig, ResolvedCrateConfig};
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct WorkspacePackageInheritance {
pub version: bool,
pub readme: bool,
pub keywords: bool,
pub categories: bool,
pub license: bool,
pub license_file: bool,
pub authors: bool,
}
fn read_workspace_package_fields(dir: &std::path::Path) -> Option<WorkspacePackageInheritance> {
let contents = std::fs::read_to_string(dir.join("Cargo.toml")).ok()?;
let doc = toml::from_str::<toml::Value>(&contents).ok()?;
let pkg = doc.get("workspace")?.get("package")?;
Some(WorkspacePackageInheritance {
version: pkg.get("version").is_some(),
readme: pkg.get("readme").is_some(),
keywords: pkg.get("keywords").is_some(),
categories: pkg.get("categories").is_some(),
license: pkg.get("license").is_some(),
license_file: pkg.get("license-file").is_some(),
authors: pkg.get("authors").is_some(),
})
}
fn crate_dir_is_excluded(root_doc: &toml::Value, crate_relative_dir: &str) -> bool {
let Some(excludes) = root_doc
.get("workspace")
.and_then(|w| w.get("exclude"))
.and_then(|e| e.as_array())
else {
return false;
};
let normalized = crate_relative_dir.trim_matches('/');
excludes.iter().filter_map(|entry| entry.as_str()).any(|pattern| {
let pattern = pattern.trim_matches('/');
normalized == pattern || normalized.starts_with(&format!("{pattern}/"))
})
}
pub(crate) fn detect_workspace_inheritance_for_crate(
workspace_root: Option<&std::path::Path>,
crate_relative_dir: &str,
) -> WorkspacePackageInheritance {
let Some(root) = workspace_root else {
return WorkspacePackageInheritance::default();
};
let root_reaches = std::fs::read_to_string(root.join("Cargo.toml"))
.ok()
.and_then(|contents| toml::from_str::<toml::Value>(&contents).ok())
.filter(|doc| doc.get("workspace").is_some())
.is_some_and(|doc| !crate_dir_is_excluded(&doc, crate_relative_dir));
if root_reaches && let Some(inheritance) = read_workspace_package_fields(root) {
return inheritance;
}
read_workspace_package_fields(&root.join(crate_relative_dir)).unwrap_or_default()
}
pub(crate) fn cargo_package_header(
name: &str,
version: &str,
edition: &str,
meta: &ScaffoldMeta,
ws: &WorkspacePackageInheritance,
cargo_filters: Option<&CargoPackageFilesConfig>,
) -> String {
let version_line = if ws.version {
"version.workspace = true".to_string()
} else {
format!("version = \"{version}\"")
};
let edition_line = format!("edition = \"{edition}\"");
let license_line = if ws.license {
Some("license.workspace = true".to_string())
} else if ws.license_file {
Some("license-file.workspace = true".to_string())
} else {
meta.license.as_ref().map(|license| format!("license = \"{license}\""))
};
let readme_line = if ws.readme {
"readme.workspace = true".to_string()
} else {
"readme = false".to_string()
};
let keywords_line = if ws.keywords {
"keywords.workspace = true".to_string()
} else if meta.keywords.is_empty() {
"keywords = []".to_string()
} else {
let quoted: Vec<String> = meta.keywords.iter().map(|k| format!("\"{k}\"")).collect();
format!("keywords = [{}]", quoted.join(", "))
};
let categories_line = if ws.categories {
"categories.workspace = true".to_string()
} else if meta.categories.is_empty() {
"categories = []".to_string()
} else {
let quoted: Vec<String> = meta.categories.iter().map(|k| format!("\"{k}\"")).collect();
format!("categories = [{}]", quoted.join(", "))
};
let mut lines = vec![
"[package]".to_string(),
format!("name = \"{name}\""),
version_line,
edition_line,
format!("description = \"{}\"", meta.description),
readme_line,
keywords_line,
categories_line,
];
let package_file_filters = render_cargo_package_file_filters(cargo_filters);
if !package_file_filters.is_empty() {
lines.push(package_file_filters);
}
if let Some(license_line) = license_line {
lines.insert(4, license_line);
}
if ws.authors {
lines.insert(4, "authors.workspace = true".to_string());
} else if !meta.authors.is_empty() {
let authors = toml::Table::from_iter([(
"authors".to_string(),
toml::Value::Array(meta.authors.iter().cloned().map(toml::Value::String).collect()),
)]);
let authors_line = toml::to_string(&authors).expect("string authors metadata serializes to TOML");
lines.insert(4, authors_line.trim_end().to_string());
}
lines.join("\n")
}
pub struct ScaffoldMeta {
pub description: String,
pub license: Option<String>,
pub repository: Option<String>,
pub configured_repository: Option<String>,
pub homepage: String,
pub documentation: String,
pub issues: String,
pub funding: String,
pub authors: Vec<String>,
pub keywords: Vec<String>,
pub categories: Vec<String>,
}
pub fn scaffold_meta(config: &ResolvedCrateConfig) -> ScaffoldMeta {
let scaffold = config.scaffold.as_ref();
let package = config.package_metadata.as_ref();
let truncate = package.map(|p| p.truncate_registry_lists).unwrap_or(false);
let configured_repository = package
.and_then(|p| p.repository.clone())
.or_else(|| scaffold.and_then(|s| s.repository.clone()));
let mut keywords = package
.filter(|p| !p.keywords.is_empty())
.map(|p| p.keywords.clone())
.or_else(|| scaffold.map(|s| s.keywords.clone()))
.unwrap_or_default();
let mut categories = package.map(|p| p.categories.clone()).unwrap_or_default();
keywords.sort();
categories.sort();
if truncate {
keywords.truncate(5);
categories.truncate(5);
}
ScaffoldMeta {
description: package
.and_then(|p| p.description.clone())
.or_else(|| scaffold.and_then(|s| s.description.clone()))
.unwrap_or_else(|| format!("Bindings for {}", config.name)),
license: package
.and_then(|p| p.license.clone())
.or_else(|| scaffold.and_then(|s| s.license.clone())),
repository: configured_repository.clone(),
configured_repository,
homepage: package
.and_then(|p| p.homepage.clone())
.or_else(|| scaffold.and_then(|s| s.homepage.clone()))
.unwrap_or_default(),
documentation: package.and_then(|p| p.documentation.clone()).unwrap_or_default(),
issues: package.and_then(|p| p.issues.clone()).unwrap_or_default(),
funding: package.and_then(|p| p.funding.clone()).unwrap_or_default(),
authors: package
.filter(|p| !p.authors.is_empty())
.map(|p| p.authors.clone())
.or_else(|| scaffold.map(|s| s.authors.clone()))
.unwrap_or_default(),
keywords,
categories,
}
}
pub(crate) fn render_cargo_package_file_filters(config: Option<&CargoPackageFilesConfig>) -> String {
let Some(config) = config else {
return String::new();
};
let (name, values) = if !config.include.is_empty() {
("include", &config.include)
} else if !config.exclude.is_empty() {
("exclude", &config.exclude)
} else {
return String::new();
};
let table = toml::Table::from_iter([(
name.to_string(),
toml::Value::Array(values.iter().cloned().map(toml::Value::String).collect()),
)]);
toml::to_string(&table)
.expect("Cargo package file filters serialize to TOML")
.trim_end()
.to_string()
}
pub(crate) fn cargo_package_file_filters(config: &ResolvedCrateConfig) -> Option<&CargoPackageFilesConfig> {
config
.package_metadata
.as_ref()
.and_then(|metadata| metadata.cargo.as_ref())
}
pub(crate) fn readme_language_configured(config: &ResolvedCrateConfig, lang_code: &str) -> bool {
config
.readme
.as_ref()
.is_some_and(|readme| readme.languages.contains_key(lang_code))
}