use crate::core::config::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(crate) fn detect_workspace_inheritance(workspace_root: Option<&std::path::Path>) -> WorkspacePackageInheritance {
let cargo_toml_path = workspace_root
.map(|r| r.join("Cargo.toml"))
.unwrap_or_else(|| std::path::PathBuf::from("Cargo.toml"));
let Ok(contents) = std::fs::read_to_string(&cargo_toml_path) else {
return WorkspacePackageInheritance::default();
};
let Ok(doc) = toml::from_str::<toml::Value>(&contents) else {
return WorkspacePackageInheritance::default();
};
let Some(workspace) = doc.get("workspace") else {
return WorkspacePackageInheritance::default();
};
let pkg = workspace.get("package");
WorkspacePackageInheritance {
version: pkg.map(|p| p.get("version").is_some()).unwrap_or(false),
readme: pkg.map(|p| p.get("readme").is_some()).unwrap_or(false),
keywords: pkg.map(|p| p.get("keywords").is_some()).unwrap_or(false),
categories: pkg.map(|p| p.get("categories").is_some()).unwrap_or(false),
license: pkg.map(|p| p.get("license").is_some()).unwrap_or(false),
}
}
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(),
})
}
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,
) -> 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 {
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,
];
if let Some(license_line) = license_line {
lines.insert(4, license_line);
}
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 readme_language_configured(config: &ResolvedCrateConfig, lang_code: &str) -> bool {
config
.readme
.as_ref()
.is_some_and(|readme| readme.languages.contains_key(lang_code))
}