use crate::error::{Result, VersionError};
use semver::Version;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use toml_edit::{DocumentMut, Item, Value, InlineTable};
#[derive(Debug)]
pub struct TomlEditor {
file_path: std::path::PathBuf,
document: DocumentMut,
original_content: String,
}
#[derive(Debug, Clone)]
pub struct TomlBackup {
pub file_path: std::path::PathBuf,
pub content: String,
}
impl TomlEditor {
pub fn open<P: AsRef<Path>>(file_path: P) -> Result<Self> {
let file_path = file_path.as_ref().to_path_buf();
let content = std::fs::read_to_string(&file_path)
.map_err(|e| std::io::Error::new(e.kind(), format!("Failed to read {}: {}", file_path.display(), e)))?;
let document = content.parse::<DocumentMut>()
.map_err(|e| VersionError::TomlUpdateFailed {
path: file_path.clone(),
reason: format!("Failed to parse TOML: {}", e),
})?;
Ok(Self {
file_path,
document,
original_content: content,
})
}
pub fn update_package_version(&mut self, new_version: &Version) -> Result<()> {
let version_str = new_version.to_string();
let package_table = self.document.get_mut("package")
.and_then(|item| item.as_table_mut())
.ok_or_else(|| VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "No [package] section found".to_string(),
})?;
match package_table.get_mut("version") {
Some(version_item) => {
match version_item {
Item::Value(Value::String(formatted_string)) => {
*formatted_string = toml_edit::Formatted::new(version_str);
}
Item::Value(Value::InlineTable(table)) => {
if table.contains_key("workspace") {
return Ok(());
}
return Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "Unexpected version format in inline table".to_string(),
}.into());
}
Item::Table(table) => {
if table.contains_key("workspace") {
return Ok(());
}
return Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "Unexpected version format in table".to_string(),
}.into());
}
_ => {
return Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "Version field has unexpected format".to_string(),
}.into());
}
}
}
None => {
package_table.insert("version", toml_edit::value(version_str));
}
}
Ok(())
}
pub fn update_workspace_version(&mut self, new_version: &Version) -> Result<()> {
let version_str = new_version.to_string();
let workspace_table = self.document.get_mut("workspace")
.and_then(|item| item.as_table_mut())
.ok_or_else(|| VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "No [workspace] section found".to_string(),
})?;
let package_table = workspace_table.get_mut("package")
.and_then(|item| item.as_table_mut())
.ok_or_else(|| VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "No [workspace.package] section found".to_string(),
})?;
match package_table.get_mut("version") {
Some(version_item) => {
if let Item::Value(Value::String(formatted_string)) = version_item {
*formatted_string = toml_edit::Formatted::new(version_str);
} else {
return Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "Workspace version field has unexpected format".to_string(),
}.into());
}
}
None => {
package_table.insert("version", toml_edit::value(version_str));
}
}
Ok(())
}
pub fn update_dependency_version(&mut self, dependency_name: &str, new_version: &Version) -> Result<()> {
let version_str = new_version.to_string();
let mut updated = false;
if let Some(deps_table) = self.document.get_mut("dependencies").and_then(|item| item.as_table_mut()) {
if let Some(dep_item) = deps_table.get_mut(dependency_name) {
Self::update_dependency_item(dep_item, &version_str, &self.file_path)?;
updated = true;
}
}
if let Some(dev_deps_table) = self.document.get_mut("dev-dependencies").and_then(|item| item.as_table_mut()) {
if let Some(dep_item) = dev_deps_table.get_mut(dependency_name) {
Self::update_dependency_item(dep_item, &version_str, &self.file_path)?;
updated = true;
}
}
if let Some(build_deps_table) = self.document.get_mut("build-dependencies").and_then(|item| item.as_table_mut()) {
if let Some(dep_item) = build_deps_table.get_mut(dependency_name) {
Self::update_dependency_item(dep_item, &version_str, &self.file_path)?;
updated = true;
}
}
if !updated {
return Err(VersionError::DependencyMismatch {
dependency: dependency_name.to_string(),
expected: version_str,
found: "not found".to_string(),
}.into());
}
Ok(())
}
fn update_dependency_item(dep_item: &mut Item, version_str: &str, file_path: &PathBuf) -> Result<()> {
match dep_item {
Item::Value(Value::String(version_ref)) => {
*version_ref = toml_edit::Formatted::new(version_str.to_string());
}
Item::Value(Value::InlineTable(table)) => {
if table.contains_key("version") {
table.insert("version", toml_edit::Value::from(version_str));
} else {
table.insert("version", toml_edit::Value::from(version_str));
}
}
Item::Table(table) => {
if table.contains_key("version") {
table.insert("version", toml_edit::value(version_str));
} else {
table.insert("version", toml_edit::value(version_str));
}
}
_ => {
return Err(VersionError::TomlUpdateFailed {
path: file_path.clone(),
reason: format!("Unexpected dependency format for item: {:?}", dep_item),
}.into());
}
}
Ok(())
}
pub fn update_multiple_dependencies(&mut self, updates: &HashMap<String, Version>) -> Result<()> {
for (dep_name, version) in updates {
self.update_dependency_version(dep_name, version)?;
}
Ok(())
}
pub fn add_dependency(
&mut self,
dependency_name: &str,
version: &Version,
section: DependencySection,
additional_fields: Option<HashMap<String, String>>,
) -> Result<()> {
let section_name = section.section_name();
if !self.document.contains_key(section_name) {
self.document.insert(section_name, toml_edit::Item::Table(toml_edit::Table::new()));
}
let deps_table = self.document.get_mut(section_name)
.and_then(|item| item.as_table_mut())
.ok_or_else(|| VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: format!("Failed to access {} section", section_name),
})?;
if let Some(additional) = additional_fields {
let mut inline_table = InlineTable::new();
inline_table.insert("version", toml_edit::Value::from(version.to_string()));
for (key, value) in additional {
inline_table.insert(&key, toml_edit::Value::from(value));
}
deps_table.insert(dependency_name, toml_edit::Item::Value(Value::InlineTable(inline_table)));
} else {
deps_table.insert(dependency_name, toml_edit::value(version.to_string()));
}
Ok(())
}
pub fn remove_dependency(&mut self, dependency_name: &str, section: DependencySection) -> Result<bool> {
let section_name = section.section_name();
if let Some(deps_table) = self.document.get_mut(section_name).and_then(|item| item.as_table_mut()) {
Ok(deps_table.remove(dependency_name).is_some())
} else {
Ok(false)
}
}
pub fn get_current_version(&self) -> Result<Version> {
if let Some(package_table) = self.document.get("package").and_then(|item| item.as_table()) {
if let Some(version_item) = package_table.get("version") {
match version_item.as_value() {
Some(Value::String(version_str)) => {
return Version::parse(version_str.value())
.map_err(|e| VersionError::ParseFailed {
version: version_str.value().to_string(),
source: e,
}.into());
}
Some(Value::InlineTable(table)) => {
if table.contains_key("workspace") {
} else {
return Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "Package version has unexpected inline table format".to_string(),
}.into());
}
}
_ => {}
}
}
}
if let Some(workspace_table) = self.document.get("workspace").and_then(|item| item.as_table()) {
if let Some(package_table) = workspace_table.get("package").and_then(|item| item.as_table()) {
if let Some(version_item) = package_table.get("version") {
if let Some(Value::String(version_str)) = version_item.as_value() {
return Version::parse(version_str.value())
.map_err(|e| VersionError::ParseFailed {
version: version_str.value().to_string(),
source: e,
}.into());
}
}
}
}
Err(VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: "No version found in package or workspace.package sections".to_string(),
}.into())
}
pub fn uses_workspace_version(&self) -> bool {
if let Some(package_table) = self.document.get("package").and_then(|item| item.as_table()) {
if let Some(version_item) = package_table.get("version") {
if let Some(Value::InlineTable(table)) = version_item.as_value() {
return table.contains_key("workspace");
}
}
}
false
}
pub fn save(&self) -> Result<()> {
std::fs::write(&self.file_path, self.document.to_string())
.map_err(|e| VersionError::TomlUpdateFailed {
path: self.file_path.clone(),
reason: format!("Failed to write file: {}", e),
}.into())
}
pub fn create_backup(&self) -> TomlBackup {
TomlBackup {
file_path: self.file_path.clone(),
content: self.original_content.clone(),
}
}
pub fn restore_from_backup(backup: &TomlBackup) -> Result<()> {
std::fs::write(&backup.file_path, &backup.content)
.map_err(|e| VersionError::TomlUpdateFailed {
path: backup.file_path.clone(),
reason: format!("Failed to restore backup: {}", e),
}.into())
}
pub fn preview(&self) -> String {
self.document.to_string()
}
pub fn file_path(&self) -> &Path {
&self.file_path
}
pub fn is_modified(&self) -> bool {
self.document.to_string() != self.original_content
}
pub fn get_all_dependencies(&self) -> HashMap<String, DependencyInfo> {
let mut dependencies = HashMap::new();
let extract_deps = |table: &toml_edit::Table, section: DependencySection| -> Vec<(String, DependencyInfo)> {
let mut deps = Vec::new();
for (name, item) in table.iter() {
if let Some(dep_info) = self.parse_dependency_info(item, section) {
deps.push((name.to_string(), dep_info));
}
}
deps
};
if let Some(deps_table) = self.document.get("dependencies").and_then(|item| item.as_table()) {
for (name, dep_info) in extract_deps(deps_table, DependencySection::Dependencies) {
dependencies.insert(name, dep_info);
}
}
if let Some(dev_deps_table) = self.document.get("dev-dependencies").and_then(|item| item.as_table()) {
for (name, dep_info) in extract_deps(dev_deps_table, DependencySection::DevDependencies) {
dependencies.insert(name, dep_info);
}
}
if let Some(build_deps_table) = self.document.get("build-dependencies").and_then(|item| item.as_table()) {
for (name, dep_info) in extract_deps(build_deps_table, DependencySection::BuildDependencies) {
dependencies.insert(name, dep_info);
}
}
dependencies
}
fn parse_dependency_info(&self, item: &Item, section: DependencySection) -> Option<DependencyInfo> {
match item {
Item::Value(Value::String(version_str)) => {
Some(DependencyInfo {
version: Some(version_str.value().to_string()),
path: None,
git: None,
section,
})
}
Item::Value(Value::InlineTable(table)) => {
let version = table.get("version").and_then(|v| v.as_str()).map(|s| s.to_string());
let path = table.get("path").and_then(|v| v.as_str()).map(|s| s.to_string());
let git = table.get("git").and_then(|v| v.as_str()).map(|s| s.to_string());
Some(DependencyInfo {
version,
path,
git,
section,
})
}
Item::Table(table) => {
let version = table.get("version").and_then(|item| item.as_value()).and_then(|v| v.as_str()).map(|s| s.to_string());
let path = table.get("path").and_then(|item| item.as_value()).and_then(|v| v.as_str()).map(|s| s.to_string());
let git = table.get("git").and_then(|item| item.as_value()).and_then(|v| v.as_str()).map(|s| s.to_string());
Some(DependencyInfo {
version,
path,
git,
section,
})
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DependencySection {
Dependencies,
DevDependencies,
BuildDependencies,
}
impl DependencySection {
pub fn section_name(&self) -> &'static str {
match self {
DependencySection::Dependencies => "dependencies",
DependencySection::DevDependencies => "dev-dependencies",
DependencySection::BuildDependencies => "build-dependencies",
}
}
}
#[derive(Debug, Clone)]
pub struct DependencyInfo {
pub version: Option<String>,
pub path: Option<String>,
pub git: Option<String>,
pub section: DependencySection,
}
impl DependencyInfo {
pub fn is_path_dependency(&self) -> bool {
self.path.is_some()
}
pub fn is_git_dependency(&self) -> bool {
self.git.is_some()
}
pub fn has_version(&self) -> bool {
self.version.is_some()
}
}