1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#[cfg(feature = "miette")]
use miette::Diagnostic;
use relative_path::RelativePathBuf;
use serde::Deserialize;
use thiserror::Error;
use toml::Spanned;

use crate::{action::Action, semver::Version};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Cargo {
    path: RelativePathBuf,
    raw_toml: String,
    parsed: Toml,
}

impl Cargo {
    /// Parses the raw TOML to determine the package version.
    ///
    /// # Errors
    ///
    /// If the TOML is invalid or missing the `package.version` property.
    pub fn new(path: RelativePathBuf, raw_toml: String) -> Result<Self, Error> {
        match toml::from_str::<Toml>(&raw_toml) {
            Ok(parsed) => Ok(Cargo {
                path,
                raw_toml,
                parsed,
            }),
            Err(err) => Err(Error::Deserialize { path, source: err }),
        }
    }

    #[must_use]
    pub fn get_version(&self) -> &Version {
        self.parsed.package.version.as_ref()
    }

    #[must_use]
    pub fn get_path(&self) -> &RelativePathBuf {
        &self.path
    }

    #[must_use]
    pub fn get_package_name(&self) -> &str {
        &self.parsed.package.name
    }

    #[must_use]
    pub fn set_version(mut self, new_version: &Version) -> Action {
        let start = self.parsed.package.version.span().start + 1;
        let end = self.parsed.package.version.span().end - 1;
        let version_str = new_version.to_string();

        self.raw_toml.replace_range(start..end, &version_str);
        Action::WriteToFile {
            path: self.path,
            content: self.raw_toml,
            diff: version_str,
        }
    }
}

#[derive(Debug, Error)]
#[cfg_attr(feature = "miette", derive(Diagnostic))]
pub enum Error {
    #[error("Error deserializing {path}: {source}")]
    #[cfg_attr(feature = "miette", diagnostic(
        code(cargo::deserialize),
        help("Knope expects the Cargo.toml file to have `package.version` and `package.name` properties."),
        url("https://knope.tech/reference/config-file/packages/#cargotoml")
    ))]
    Deserialize {
        path: RelativePathBuf,
        #[source]
        source: toml::de::Error,
    },
    #[error(transparent)]
    #[cfg_attr(feature = "miette", diagnostic(transparent))]
    Semver(#[from] crate::semver::Error),
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Toml {
    pub package: Package,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct Package {
    pub name: String,
    version: Spanned<Version>,
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use std::str::FromStr;

    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_set_version() {
        let content = r#"
        [package]
        name = "tester"
        version = "0.1.0-rc.0"
        
        [dependencies]
        knope-versioning = "0.1.0"
        "#;

        let new = Cargo::new(RelativePathBuf::from("beep/boop"), String::from(content)).unwrap();

        let new_version = "1.2.3-rc.4";
        let expected = content.replace("0.1.0-rc.0", new_version);
        let expected = Action::WriteToFile {
            path: RelativePathBuf::from("beep/boop"),
            content: expected,
            diff: new_version.to_string(),
        };
        let new = new.set_version(&Version::from_str(new_version).unwrap());

        assert_eq!(new, expected);
    }
}