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