knope_versioning/versioned_file/
cargo.rs1#[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 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}