use crate::cargo::manifest_ops;
use crate::error::{RailError, RailResult};
use cargo_metadata::Metadata;
use std::cell::RefCell;
use std::path::PathBuf;
use toml_edit::{DocumentMut, Item, Table};
pub struct TransformContext {
pub crate_name: String,
pub workspace_root: PathBuf,
pub target_has_workspace: bool,
}
pub struct CargoTransform {
metadata: Metadata,
cached_workspace_doc: RefCell<Option<DocumentMut>>,
workspace_root: PathBuf,
}
impl CargoTransform {
pub fn new(metadata: Metadata) -> Self {
let workspace_root = metadata.workspace_root.as_std_path().to_path_buf();
Self {
metadata,
cached_workspace_doc: RefCell::new(None),
workspace_root,
}
}
fn ensure_workspace_doc_cached(&self) -> RailResult<()> {
let already_cached = self.cached_workspace_doc.borrow().is_some();
if !already_cached {
let workspace_toml_path = self.workspace_root.join("Cargo.toml");
let doc = manifest_ops::read_toml_file(&workspace_toml_path)?;
*self.cached_workspace_doc.borrow_mut() = Some(doc);
}
Ok(())
}
fn get_workspace_package(&self) -> RailResult<Option<Table>> {
self.ensure_workspace_doc_cached()?;
let cache = self.cached_workspace_doc.borrow();
Ok(
cache
.as_ref()
.and_then(|doc| doc.get("workspace"))
.and_then(|w| w.as_table())
.and_then(|w| w.get("package"))
.and_then(|p| p.as_table())
.cloned(),
)
}
fn get_workspace_lints(&self) -> RailResult<Option<Item>> {
self.ensure_workspace_doc_cached()?;
let cache = self.cached_workspace_doc.borrow();
Ok(
cache
.as_ref()
.and_then(|doc| doc.get("workspace"))
.and_then(|w| w.as_table())
.and_then(|w| w.get("lints"))
.cloned(),
)
}
pub fn transform_to_split(&self, content: &str, context: &TransformContext) -> RailResult<String> {
let mut doc: DocumentMut = content
.parse()
.map_err(|e| RailError::message(format!("Failed to parse Cargo.toml: {}", e)))?;
self.resolve_workspace_inheritance(&mut doc)?;
self.transform_dependencies_to_standalone(&mut doc)?;
if !context.target_has_workspace {
self.resolve_lints_workspace_inheritance(&mut doc)?;
}
Ok(doc.to_string())
}
pub fn transform_to_mono(&self, content: &str, _context: &TransformContext) -> RailResult<String> {
Ok(content.to_string())
}
fn resolve_workspace_inheritance(&self, doc: &mut DocumentMut) -> RailResult<()> {
if let Some(workspace_pkg) = self.get_workspace_package()? {
manifest_ops::resolve_package_workspace_inheritance(doc, &workspace_pkg)?;
}
Ok(())
}
fn resolve_lints_workspace_inheritance(&self, doc: &mut DocumentMut) -> RailResult<()> {
let has_workspace_lints = doc
.get("lints")
.and_then(|l| l.as_table())
.and_then(|t| t.get("workspace"))
.and_then(|w| w.as_value())
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !has_workspace_lints {
return Ok(());
}
let workspace_lints = self.get_workspace_lints()?;
doc.remove("lints");
if let Some(ws_lints) = workspace_lints {
doc.insert("lints", ws_lints);
}
Ok(())
}
fn transform_dependencies_to_standalone(&self, doc: &mut DocumentMut) -> RailResult<()> {
manifest_ops::transform_dependencies_in_section(doc, "dependencies", |name, item| {
self.transform_and_resolve_dep(item, name)
})?;
manifest_ops::transform_dependencies_in_section(doc, "dev-dependencies", |name, item| {
self.transform_and_resolve_dep(item, name)
})?;
manifest_ops::transform_dependencies_in_section(doc, "build-dependencies", |name, item| {
self.transform_and_resolve_dep(item, name)
})?;
Ok(())
}
fn transform_and_resolve_dep(&self, dep_item: &mut Item, dep_name: &str) -> RailResult<()> {
if manifest_ops::is_workspace_dep(dep_item) {
if let Some(pkg) = self.metadata.packages.iter().find(|p| p.name == dep_name) {
let version = pkg.version.to_string();
manifest_ops::extract_workspace_marker(dep_item);
manifest_ops::set_version(dep_item, &version)?;
}
}
manifest_ops::remove_path(dep_item);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transform_workspace_dep() {
let input = r#"
[package]
name = "test-crate"
version = "0.1.0"
[dependencies]
other-crate = { workspace = true }
serde = "1.0"
"#;
let doc: DocumentMut = input.parse().unwrap();
assert!(doc.get("dependencies").is_some());
}
#[test]
fn test_transform_to_mono_passthrough() {
let input = r#"
[package]
name = "test-crate"
version = "0.1.0"
[dependencies]
serde = "1.0"
"#;
let metadata_json = serde_json::json!({
"packages": [],
"workspace_members": [],
"resolve": null,
"target_directory": "/tmp",
"version": 1,
"workspace_root": "/tmp",
"metadata": null
});
let metadata: Metadata = serde_json::from_value(metadata_json).unwrap();
let transformer = CargoTransform::new(metadata);
let context = TransformContext {
crate_name: "test-crate".to_string(),
workspace_root: PathBuf::from("/tmp"),
target_has_workspace: false,
};
let result = transformer.transform_to_mono(input, &context).unwrap();
assert_eq!(result, input);
}
#[test]
fn test_resolve_lints_workspace_inheritance_removes_workspace_true() {
let input = r#"
[package]
name = "test-crate"
version = "0.1.0"
[lints]
workspace = true
"#;
let mut doc: DocumentMut = input.parse().unwrap();
assert!(doc.get("lints").is_some());
let lints = doc.get("lints").unwrap().as_table().unwrap();
assert!(lints.get("workspace").is_some());
doc.remove("lints");
assert!(doc.get("lints").is_none());
}
#[test]
fn test_resolve_lints_workspace_inheritance_copies_workspace_lints() {
let input = r#"
[package]
name = "test-crate"
version = "0.1.0"
[lints]
workspace = true
"#;
let mut doc: DocumentMut = input.parse().unwrap();
let workspace_lints = r#"
[rust]
unexpected_cfgs = { level = "warn" }
"#;
let ws_lints_doc: DocumentMut = workspace_lints.parse().unwrap();
doc.remove("lints");
if let Some(lints_item) = ws_lints_doc.as_item().as_table() {
doc.insert("lints", Item::Table(lints_item.clone()));
}
let lints = doc.get("lints").unwrap().as_table().unwrap();
assert!(lints.get("rust").is_some());
assert!(lints.get("workspace").is_none()); }
}