knope_versioning/versioned_file/
cargo.rs

1#[cfg(feature = "miette")]
2use miette::Diagnostic;
3use relative_path::RelativePathBuf;
4use thiserror::Error;
5use toml_edit::{DocumentMut, TomlError, value};
6
7use crate::{Action, semver::Version};
8
9#[derive(Clone, Debug)]
10pub struct Cargo {
11    pub(super) path: RelativePathBuf,
12    pub(crate) document: DocumentMut,
13    diff: Vec<String>,
14}
15
16impl Cargo {
17    /// Parses the raw TOML to determine the package version.
18    ///
19    /// # Errors
20    ///
21    /// If the TOML is invalid or missing a required property.
22    pub fn new(path: RelativePathBuf, toml: &str) -> Result<Self, Error> {
23        let document: DocumentMut = toml.parse().map_err(|source| Error::Toml {
24            source,
25            path: path.clone(),
26        })?;
27        Ok(Self {
28            path,
29            document,
30            diff: Vec::new(),
31        })
32    }
33
34    pub(super) fn get_version(&self) -> Result<Version, Error> {
35        self.document
36            .get("package")
37            .and_then(|package| package.get("version")?.as_str())
38            .ok_or_else(|| Error::MissingRequiredProperties {
39                property: "package.version",
40                path: self.path.clone(),
41            })?
42            .parse()
43            .map_err(Error::Semver)
44    }
45
46    #[must_use]
47    pub(super) fn set_version(mut self, new_version: &Version, dependency: Option<&str>) -> Self {
48        let diff = if let Some(dependency) = dependency {
49            if let Some(dep) = self
50                .document
51                .get_mut("dependencies")
52                .and_then(|deps| deps.get_mut(dependency))
53            {
54                write_version_to_dep(dep, new_version);
55            }
56            if let Some(dep) = self
57                .document
58                .get_mut("dev-dependencies")
59                .and_then(|deps| deps.get_mut(dependency))
60            {
61                write_version_to_dep(dep, new_version);
62            }
63            if let Some(dep) = self
64                .document
65                .get_mut("workspace")
66                .and_then(|workspace| workspace.get_mut("dependencies")?.get_mut(dependency))
67            {
68                write_version_to_dep(dep, new_version);
69            }
70            format!("{dependency}.version = {new_version}")
71        } else {
72            let version = self
73                .document
74                .get_mut("package")
75                .and_then(|package| package.get_mut("version"));
76            if let Some(version) = version {
77                *version = value(new_version.to_string());
78            }
79            format!("version = {new_version}")
80        };
81        self.diff.push(diff);
82        self
83    }
84
85    pub(super) fn write(self) -> Option<Action> {
86        if self.diff.is_empty() {
87            return None;
88        }
89        Some(Action::WriteToFile {
90            path: self.path,
91            content: self.document.to_string(),
92            diff: self.diff.join(", "),
93        })
94    }
95}
96
97#[must_use]
98pub fn name_from_document(document: &DocumentMut) -> Option<&str> {
99    document
100        .get("package")
101        .and_then(|package| package.get("name")?.as_str())
102}
103
104#[must_use]
105pub fn contains_dependency(document: &DocumentMut, dependency: &str) -> bool {
106    document
107        .get("dependencies")
108        .and_then(|deps| deps.get(dependency))
109        .is_some()
110        || document
111            .get("dev-dependencies")
112            .and_then(|deps| deps.get(dependency))
113            .is_some()
114        || document
115            .get("workspace")
116            .and_then(|workspace| workspace.get("dependencies")?.get(dependency))
117            .is_some()
118}
119
120#[cfg(test)]
121mod test_contains_dependency {
122    use super::*;
123    #[test]
124    fn basic_dependency() {
125        let content = r#"
126        [package]
127        name = "tester"
128        version = "1.2.3-rc.0"
129        
130        [dependencies]
131        knope-versioning = "0.1.0"
132        "#;
133
134        let document: DocumentMut = content.parse().expect("valid toml");
135        assert!(contains_dependency(&document, "knope-versioning"));
136    }
137
138    #[test]
139    fn inline_table_dependency() {
140        let content = r#"
141        [package]
142        name = "tester"
143        version = "1.2.3-rc.0"
144        
145        [dependencies]
146        knope-versioning = { version = "0.1.0" }
147        "#;
148
149        let document: DocumentMut = content.parse().expect("valid toml");
150        assert!(contains_dependency(&document, "knope-versioning"));
151    }
152
153    #[test]
154    fn table_dependency() {
155        let content = r#"
156        [package]
157        name = "tester"
158        version = "1.2.3-rc.0"
159        
160        [dependencies.knope-versioning]
161        path = "../knope-versioning"
162        version = "0.1.0"
163        "#;
164
165        let document: DocumentMut = content.parse().expect("valid toml");
166        assert!(contains_dependency(&document, "knope-versioning"));
167    }
168
169    #[test]
170    fn dev_dependency() {
171        let content = r#"
172        [package]
173        name = "tester"
174        version = "1.2.3-rc.0"
175        
176        [dev-dependencies]
177        knope-versioning = "0.1.0"
178        "#;
179
180        let document: DocumentMut = content.parse().expect("valid toml");
181        assert!(contains_dependency(&document, "knope-versioning"));
182    }
183
184    #[test]
185    fn workspace_dependency() {
186        let content = r#"
187        [package]
188        name = "tester"
189        version = "1.2.3-rc.0"
190        
191        [workspace.dependencies]
192        knope-versioning = "0.1.0"
193        "#;
194
195        let document: DocumentMut = content.parse().expect("valid toml");
196        assert!(contains_dependency(&document, "knope-versioning"));
197    }
198}
199
200fn write_version_to_dep(dep: &mut toml_edit::Item, version: &Version) {
201    if let Some(table) = dep.as_table_mut() {
202        table.insert("version", value(version.to_string()));
203    } else if let Some(table) = dep.as_inline_table_mut() {
204        table.insert("version", version.to_string().into());
205    } else if let Some(value) = dep.as_value_mut() {
206        *value = version.to_string().into();
207    }
208}
209
210#[derive(Debug, Error)]
211#[cfg_attr(feature = "miette", derive(Diagnostic))]
212pub enum Error {
213    #[error("Invalid TOML in {path}: {source}")]
214    #[cfg_attr(feature = "miette", diagnostic(code(knope_versioning::cargo::toml),))]
215    Toml {
216        path: RelativePathBuf,
217        #[source]
218        source: TomlError,
219    },
220    #[error("{path} was missing required property {property}")]
221    #[cfg_attr(
222        feature = "miette",
223        diagnostic(
224            code(knope_versioning::cargo::missing_property),
225            url("https://knope.tech/reference/config-file/packages/#cargotoml")
226        )
227    )]
228    MissingRequiredProperties {
229        path: RelativePathBuf,
230        property: &'static str,
231    },
232    #[error("{path} does not contain dependency {dependency}")]
233    #[cfg_attr(
234        feature = "miette",
235        diagnostic(
236            code(knope_versioning::cargo::missing_dependency),
237            url("https://knope.tech/reference/config-file/packages/#cargotoml")
238        )
239    )]
240    MissingDependency {
241        path: RelativePathBuf,
242        dependency: String,
243    },
244    #[error(transparent)]
245    #[cfg_attr(feature = "miette", diagnostic(transparent))]
246    Semver(#[from] crate::semver::Error),
247}
248
249#[derive(Clone, Debug, Eq, PartialEq)]
250pub struct Toml {
251    package_name: String,
252    version: Version,
253    version_path: String,
254}
255
256#[cfg(test)]
257#[allow(clippy::unwrap_used)]
258mod tests {
259    use std::str::FromStr;
260
261    use pretty_assertions::assert_eq;
262
263    use super::*;
264    use crate::Action;
265
266    #[test]
267    fn set_package_version() {
268        let content = r#"
269        [package]
270        name = "tester"
271        version = "0.1.0-rc.0"
272        
273        [dependencies]
274        knope-versioning = "0.1.0"
275        "#;
276
277        let new = Cargo::new(RelativePathBuf::from("beep/Cargo.toml"), content).unwrap();
278
279        let new_version = "1.2.3-rc.4";
280        let expected = content.replace("0.1.0-rc.0", new_version);
281        let new = new.set_version(&Version::from_str(new_version).unwrap(), None);
282
283        assert_eq!(new.document.to_string(), expected);
284    }
285
286    #[test]
287    fn dependencies() {
288        let content = r#"
289        [package]
290        name = "tester"
291        version = "1.2.3-rc.0"
292        
293        [dependencies]
294        knope-versioning = "0.1.0"
295        other = {path = "../other"}
296        complex-requirement = "3.*"
297        complex-requirement-in-object = { version = "1.2.*" }
298        
299        [dev-dependencies]
300        knope-versioning = {path = "../blah", version = "0.1.0" }
301        
302        [workspace.dependencies]
303        knope-versioning = "0.1.0"
304        "#;
305
306        let new = Cargo::new(RelativePathBuf::from("beep/Cargo.toml"), content).unwrap();
307
308        let new = new.set_version(
309            &Version::from_str("0.2.0").unwrap(),
310            Some("knope-versioning"),
311        );
312        let expected = content.replace("0.1.0", "0.2.0");
313        let new = new.set_version(
314            &Version::from_str("2.0.0").unwrap(),
315            Some("complex-requirement-in-object"),
316        );
317        let expected = expected.replace("1.2.*", "2.0.0");
318
319        let expected = Action::WriteToFile {
320            path: RelativePathBuf::from("beep/Cargo.toml"),
321            content: expected,
322            diff: "knope-versioning.version = 0.2.0, complex-requirement-in-object.version = 2.0.0"
323                .to_string(),
324        };
325
326        assert_eq!(new.write().expect("diff to write"), expected);
327    }
328}