Skip to main content

embeddenator_workspace/
cargo.rs

1//! Cargo.toml file parsing and manipulation utilities.
2
3use anyhow::{Context, Result};
4use semver::Version;
5use std::path::{Path, PathBuf};
6use toml_edit::{value, DocumentMut, Item};
7
8/// Represents a Cargo.toml manifest file.
9#[derive(Debug, Clone)]
10pub struct CargoManifest {
11    pub path: PathBuf,
12    pub package_name: String,
13    pub version: Version,
14    pub dependencies: Vec<Dependency>,
15    document: DocumentMut,
16}
17
18/// Represents a dependency in Cargo.toml.
19#[derive(Debug, Clone)]
20pub struct Dependency {
21    pub name: String,
22    pub version: Option<Version>,
23    pub dep_type: DependencyType,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum DependencyType {
28    Normal,
29    Dev,
30    Build,
31}
32
33impl CargoManifest {
34    /// Load a Cargo.toml file from disk.
35    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
36        let path = path.as_ref();
37        let content = std::fs::read_to_string(path)
38            .with_context(|| format!("Failed to read {}", path.display()))?;
39        let document: DocumentMut = content
40            .parse()
41            .with_context(|| format!("Failed to parse {}", path.display()))?;
42
43        let package_name = document["package"]["name"]
44            .as_str()
45            .ok_or_else(|| anyhow::anyhow!("Missing package.name in {}", path.display()))?
46            .to_string();
47
48        let version_str = document["package"]["version"]
49            .as_str()
50            .ok_or_else(|| anyhow::anyhow!("Missing package.version in {}", path.display()))?;
51
52        let version = Version::parse(version_str)
53            .with_context(|| format!("Invalid version '{}' in {}", version_str, path.display()))?;
54
55        let mut dependencies = Vec::new();
56
57        // Parse dependencies
58        if let Some(Item::Table(deps)) = document.get("dependencies") {
59            for (name, item) in deps.iter() {
60                if let Some(dep) = Self::parse_dependency(name, item, DependencyType::Normal) {
61                    dependencies.push(dep);
62                }
63            }
64        }
65
66        // Parse dev-dependencies
67        if let Some(Item::Table(deps)) = document.get("dev-dependencies") {
68            for (name, item) in deps.iter() {
69                if let Some(dep) = Self::parse_dependency(name, item, DependencyType::Dev) {
70                    dependencies.push(dep);
71                }
72            }
73        }
74
75        // Parse build-dependencies
76        if let Some(Item::Table(deps)) = document.get("build-dependencies") {
77            for (name, item) in deps.iter() {
78                if let Some(dep) = Self::parse_dependency(name, item, DependencyType::Build) {
79                    dependencies.push(dep);
80                }
81            }
82        }
83
84        Ok(Self {
85            path: path.to_path_buf(),
86            package_name,
87            version,
88            dependencies,
89            document,
90        })
91    }
92
93    fn parse_dependency(name: &str, item: &Item, dep_type: DependencyType) -> Option<Dependency> {
94        let version = match item {
95            Item::Value(val) if val.is_str() => {
96                // Simple version string: "0.20.0-alpha.1"
97                val.as_str().and_then(|s| Version::parse(s).ok())
98            }
99            Item::Table(_) => {
100                // Table format: { version = "0.20.0-alpha.1", ... }
101                item.get("version")
102                    .and_then(|v| v.as_str())
103                    .and_then(|s| Version::parse(s).ok())
104            }
105            _ => None,
106        };
107
108        Some(Dependency {
109            name: name.to_string(),
110            version,
111            dep_type,
112        })
113    }
114
115    /// Update the package version.
116    pub fn set_version(&mut self, new_version: &Version) -> Result<()> {
117        self.version = new_version.clone();
118
119        if let Some(package) = self.document.get_mut("package") {
120            if let Some(pkg_table) = package.as_table_mut() {
121                pkg_table["version"] = value(new_version.to_string());
122            }
123        }
124
125        Ok(())
126    }
127
128    /// Update a dependency version.
129    pub fn update_dependency(&mut self, dep_name: &str, new_version: &Version) -> Result<()> {
130        let sections = [
131            ("dependencies", DependencyType::Normal),
132            ("dev-dependencies", DependencyType::Dev),
133            ("build-dependencies", DependencyType::Build),
134        ];
135
136        for (section, dep_type) in &sections {
137            if let Some(deps) = self.document.get_mut(section) {
138                if let Some(deps_table) = deps.as_table_mut() {
139                    if let Some(dep_item) = deps_table.get_mut(dep_name) {
140                        Self::update_dep_item_static(dep_item, new_version)?;
141
142                        // Update our internal tracking
143                        if let Some(dep) = self
144                            .dependencies
145                            .iter_mut()
146                            .find(|d| d.name == dep_name && &d.dep_type == dep_type)
147                        {
148                            dep.version = Some(new_version.clone());
149                        }
150                    }
151                }
152            }
153        }
154
155        Ok(())
156    }
157
158    fn update_dep_item_static(item: &mut Item, new_version: &Version) -> Result<()> {
159        match item {
160            Item::Value(val) if val.is_str() => {
161                // Simple string version
162                *item = value(new_version.to_string());
163            }
164            Item::Table(_) => {
165                // Table format with version key
166                if let Some(version_item) = item.get_mut("version") {
167                    *version_item = value(new_version.to_string());
168                }
169            }
170            _ => {}
171        }
172        Ok(())
173    }
174
175    /// Save the manifest back to disk.
176    pub fn save(&self) -> Result<()> {
177        std::fs::write(&self.path, self.document.to_string())
178            .with_context(|| format!("Failed to write {}", self.path.display()))?;
179        Ok(())
180    }
181
182    /// Get all embeddenator-* dependencies.
183    pub fn embeddenator_dependencies(&self) -> Vec<&Dependency> {
184        self.dependencies
185            .iter()
186            .filter(|d| d.name.starts_with("embeddenator-"))
187            .collect()
188    }
189}
190
191#[cfg(test)]
192#[path = "cargo_tests.rs"]
193mod tests;