1use std::{fmt::Debug, path::PathBuf};
2
3use relative_path::RelativePathBuf;
4use serde::{Serialize, Serializer};
5
6pub use self::go_mod::GoVersioning;
7use self::{
8 cargo::Cargo, cargo_lock::CargoLock, deno_json::DenoJson, deno_lock::DenoLock, gleam::Gleam,
9 go_mod::GoMod, maven_pom::MavenPom, package_json::PackageJson,
10 package_lock_json::PackageLockJson, pubspec::PubSpec, pyproject::PyProject,
11 regex_file::RegexFile, tauri_conf_json::TauriConfJson,
12};
13use crate::{
14 Action,
15 action::ActionSet::{Single, Two},
16 semver::Version,
17};
18
19pub mod cargo;
20mod cargo_lock;
21mod deno_json;
22mod deno_lock;
23mod gleam;
24mod go_mod;
25mod maven_pom;
26mod package_json;
27mod package_lock_json;
28mod pubspec;
29mod pyproject;
30mod regex_file;
31mod tauri_conf_json;
32
33#[derive(Clone, Debug)]
34pub enum VersionedFile {
35 Cargo(Cargo),
36 CargoLock(CargoLock),
37 DenoJson(DenoJson),
38 DenoLock(DenoLock),
39 PubSpec(PubSpec),
40 Gleam(Gleam),
41 GoMod(GoMod),
42 PackageJson(PackageJson),
43 PackageLockJson(PackageLockJson),
44 PyProject(PyProject),
45 MavenPom(MavenPom),
46 TauriConf(TauriConfJson),
47 TauriMacosConf(TauriConfJson),
48 TauriWindowsConf(TauriConfJson),
49 TauriLinuxConf(TauriConfJson),
50 RegexFile(RegexFile),
51}
52
53impl VersionedFile {
54 pub fn new<S: AsRef<str> + Debug>(
61 config: &Config,
62 content: String,
63 git_tags: &[S],
64 ) -> Result<Self, Error> {
65 match config.format {
66 Format::Cargo => Cargo::new(config.as_path(), &content)
67 .map(VersionedFile::Cargo)
68 .map_err(Error::Cargo),
69 Format::CargoLock => CargoLock::new(config.as_path(), &content)
70 .map(VersionedFile::CargoLock)
71 .map_err(Error::CargoLock),
72 Format::DenoJson => DenoJson::new(config.as_path(), content)
73 .map(VersionedFile::DenoJson)
74 .map_err(Error::DenoJson),
75 Format::DenoLock => DenoLock::new(config.as_path(), &content)
76 .map(VersionedFile::DenoLock)
77 .map_err(Error::DenoLock),
78 Format::PyProject => PyProject::new(config.as_path(), content)
79 .map(VersionedFile::PyProject)
80 .map_err(Error::PyProject),
81 Format::PubSpec => PubSpec::new(config.as_path(), content)
82 .map(VersionedFile::PubSpec)
83 .map_err(Error::PubSpec),
84 Format::Gleam => Gleam::new(config.as_path(), &content)
85 .map(VersionedFile::Gleam)
86 .map_err(Error::Gleam),
87 Format::GoMod => GoMod::new(config.as_path(), content, git_tags)
88 .map(VersionedFile::GoMod)
89 .map_err(Error::GoMod),
90 Format::PackageJson => PackageJson::new(config.as_path(), content)
91 .map(VersionedFile::PackageJson)
92 .map_err(Error::PackageJson),
93 Format::PackageLockJson => PackageLockJson::new(config.as_path(), &content)
94 .map(VersionedFile::PackageLockJson)
95 .map_err(Error::PackageLockJson),
96 Format::MavenPom => MavenPom::new(config.as_path(), content)
97 .map(VersionedFile::MavenPom)
98 .map_err(Error::MavenPom),
99 Format::TauriConf => TauriConfJson::new(config.as_path(), content)
100 .map(VersionedFile::TauriConf)
101 .map_err(Error::TauriConfJson),
102 Format::RegexFile => {
103 let patterns = config
104 .regex
105 .as_ref()
106 .ok_or_else(|| {
107 Error::RegexFile(regex_file::Error::NoMatch {
108 regex: String::new(),
109 path: config.as_path(),
110 })
111 })?
112 .clone();
113 RegexFile::new(config.as_path(), content, patterns)
114 .map(VersionedFile::RegexFile)
115 .map_err(Error::RegexFile)
116 }
117 }
118 }
119
120 #[must_use]
121 pub fn path(&self) -> &RelativePathBuf {
122 match self {
123 VersionedFile::Cargo(cargo) => &cargo.path,
124 VersionedFile::CargoLock(cargo_lock) => &cargo_lock.path,
125 VersionedFile::DenoJson(deno_json) => deno_json.get_path(),
126 VersionedFile::DenoLock(deno_lock) => deno_lock.get_path(),
127 VersionedFile::PyProject(pyproject) => &pyproject.path,
128 VersionedFile::PubSpec(pubspec) => pubspec.get_path(),
129 VersionedFile::Gleam(gleam) => &gleam.path,
130 VersionedFile::GoMod(gomod) => gomod.get_path(),
131 VersionedFile::PackageJson(package_json) => package_json.get_path(),
132 VersionedFile::PackageLockJson(package_lock_json) => package_lock_json.get_path(),
133 VersionedFile::MavenPom(maven_pom) => &maven_pom.path,
134 VersionedFile::TauriConf(tauri_conf)
135 | VersionedFile::TauriMacosConf(tauri_conf)
136 | VersionedFile::TauriWindowsConf(tauri_conf)
137 | VersionedFile::TauriLinuxConf(tauri_conf) => tauri_conf.get_path(),
138 VersionedFile::RegexFile(regex_file) => ®ex_file.path,
139 }
140 }
141
142 pub fn version(&self) -> Result<Version, Error> {
148 match self {
149 VersionedFile::Cargo(cargo) => cargo.get_version().map_err(Error::Cargo),
150 VersionedFile::CargoLock(_) | VersionedFile::DenoLock(_) => Err(Error::NoVersion),
151 VersionedFile::DenoJson(deno_json) => deno_json.get_version().ok_or(Error::NoVersion),
152 VersionedFile::PyProject(pyproject) => Ok(pyproject.version.clone()),
153 VersionedFile::PubSpec(pubspec) => Ok(pubspec.get_version().clone()),
154 VersionedFile::Gleam(gleam) => Ok(gleam.get_version().map_err(Error::Gleam)?),
155 VersionedFile::GoMod(gomod) => Ok(gomod.get_version().clone()),
156 VersionedFile::PackageJson(package_json) => Ok(package_json.get_version().clone()),
157 VersionedFile::PackageLockJson(package_lock_json) => package_lock_json
158 .get_version()
159 .map_err(Error::PackageLockJson),
160 VersionedFile::MavenPom(maven_pom) => maven_pom.get_version().map_err(Error::MavenPom),
161 VersionedFile::TauriConf(tauri_conf)
162 | VersionedFile::TauriMacosConf(tauri_conf)
163 | VersionedFile::TauriWindowsConf(tauri_conf)
164 | VersionedFile::TauriLinuxConf(tauri_conf) => Ok(tauri_conf.get_version().clone()),
165 VersionedFile::RegexFile(regex_file) => {
166 regex_file.get_version().map_err(Error::RegexFile)
167 }
168 }
169 }
170
171 pub(crate) fn set_version(
177 self,
178 new_version: &Version,
179 dependency: Option<&str>,
180 go_versioning: GoVersioning,
181 ) -> Result<Self, SetError> {
182 match self {
183 Self::Cargo(cargo) => Ok(Self::Cargo(cargo.set_version(new_version, dependency))),
184 Self::CargoLock(cargo_lock) => cargo_lock
185 .set_version(new_version, dependency)
186 .map(Self::CargoLock)
187 .map_err(SetError::CargoLock),
188 Self::DenoJson(deno_json) => deno_json
189 .set_version(new_version, dependency)
190 .map_err(SetError::Json)
191 .map(Self::DenoJson),
192 Self::DenoLock(deno_lock) => deno_lock
193 .set_version(new_version, dependency)
194 .map_err(SetError::DenoLock)
195 .map(Self::DenoLock),
196 Self::PyProject(pyproject) => Ok(Self::PyProject(pyproject.set_version(new_version))),
197 Self::PubSpec(pubspec) => pubspec
198 .set_version(new_version)
199 .map_err(SetError::Yaml)
200 .map(Self::PubSpec),
201 Self::Gleam(gleam) => Ok(Self::Gleam(gleam.set_version(new_version))),
202 Self::GoMod(gomod) => gomod
203 .set_version(new_version.clone(), go_versioning)
204 .map_err(SetError::GoMod)
205 .map(Self::GoMod),
206 Self::PackageJson(package_json) => package_json
207 .set_version(new_version, dependency)
208 .map_err(SetError::Json)
209 .map(Self::PackageJson),
210 Self::PackageLockJson(package_lock_json) => Ok(Self::PackageLockJson(
211 package_lock_json.set_version(new_version, dependency),
212 )),
213 Self::MavenPom(maven_pom) => maven_pom
214 .set_version(new_version)
215 .map_err(SetError::MavenPom)
216 .map(Self::MavenPom),
217 Self::TauriConf(tauri_conf) => tauri_conf
218 .set_version(new_version)
219 .map_err(SetError::Json)
220 .map(Self::TauriConf),
221 Self::TauriMacosConf(tauri_conf) => tauri_conf
222 .set_version(new_version)
223 .map_err(SetError::Json)
224 .map(Self::TauriMacosConf),
225 Self::TauriWindowsConf(tauri_conf) => tauri_conf
226 .set_version(new_version)
227 .map_err(SetError::Json)
228 .map(Self::TauriWindowsConf),
229 Self::TauriLinuxConf(tauri_conf) => tauri_conf
230 .set_version(new_version)
231 .map_err(SetError::Json)
232 .map(Self::TauriLinuxConf),
233 Self::RegexFile(regex_file) => Ok(Self::RegexFile(regex_file.set_version(new_version))),
234 }
235 }
236
237 pub fn write(self) -> Option<impl IntoIterator<Item = Action>> {
238 match self {
239 Self::Cargo(cargo) => cargo.write().map(Single),
240 Self::CargoLock(cargo_lock) => cargo_lock.write().map(Single),
241 Self::DenoJson(deno_json) => deno_json.write().map(Single),
242 Self::PyProject(pyproject) => pyproject.write().map(Single),
243 Self::PubSpec(pubspec) => pubspec.write().map(Single),
244 Self::Gleam(gleam) => gleam.write().map(Single),
245 Self::GoMod(gomod) => gomod.write().map(Two),
246 Self::PackageJson(package_json) => package_json.write().map(Single),
247 Self::PackageLockJson(package_lock_json) => package_lock_json.write().map(Single),
248 Self::DenoLock(deno_lock) => deno_lock.write().map(Single),
249 Self::MavenPom(maven_pom) => maven_pom.write().map(Single),
250 Self::TauriConf(tauri_conf)
251 | Self::TauriMacosConf(tauri_conf)
252 | Self::TauriWindowsConf(tauri_conf)
253 | Self::TauriLinuxConf(tauri_conf) => tauri_conf.write().map(Single),
254 Self::RegexFile(regex_file) => regex_file.write().map(Single),
255 }
256 }
257}
258
259#[derive(Debug, thiserror::Error)]
260#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
261pub enum SetError {
262 #[error("Error serializing JSON, this is a bug: {0}")]
263 #[cfg_attr(
264 feature = "miette",
265 diagnostic(
266 code(knope_versioning::versioned_file::json_serialize),
267 help("This is a bug in knope, please report it."),
268 url("https://github.com/knope-dev/knope/issues")
269 )
270 )]
271 Json(#[from] serde_json::Error),
272 #[error("Error serializing YAML, this is a bug: {0}")]
273 #[cfg_attr(
274 feature = "miette",
275 diagnostic(
276 code(knope_versioning::versioned_file::yaml_serialize),
277 help("This is a bug in knope, please report it."),
278 url("https://github.com/knope-dev/knope/issues"),
279 )
280 )]
281 Yaml(#[from] serde_yaml::Error),
282 #[error(transparent)]
283 #[cfg_attr(feature = "miette", diagnostic(transparent))]
284 GoMod(#[from] go_mod::SetError),
285 #[error(transparent)]
286 #[cfg_attr(feature = "miette", diagnostic(transparent))]
287 CargoLock(#[from] cargo_lock::SetError),
288 #[error(transparent)]
289 #[cfg_attr(feature = "miette", diagnostic(transparent))]
290 MavenPom(#[from] maven_pom::Error),
291 #[error(transparent)]
292 #[cfg_attr(feature = "miette", diagnostic(transparent))]
293 DenoLock(#[from] deno_lock::Error),
294}
295
296#[derive(Debug, thiserror::Error)]
297#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
298pub enum Error {
299 #[error("This file can't contain a version")]
300 #[cfg_attr(
301 feature = "miette",
302 diagnostic(
303 code(knope_versioning::versioned_file::no_version),
304 help("This is likely a bug, please report it."),
305 url("https://github.com/knope-dev/knope/issues")
306 )
307 )]
308 NoVersion,
309 #[error(transparent)]
310 #[cfg_attr(feature = "miette", diagnostic(transparent))]
311 Cargo(#[from] cargo::Error),
312 #[error(transparent)]
313 #[cfg_attr(feature = "miette", diagnostic(transparent))]
314 CargoLock(#[from] cargo_lock::Error),
315 #[error(transparent)]
316 #[cfg_attr(feature = "miette", diagnostic(transparent))]
317 DenoJson(#[from] deno_json::Error),
318 #[error(transparent)]
319 #[cfg_attr(feature = "miette", diagnostic(transparent))]
320 PyProject(#[from] pyproject::Error),
321 #[error(transparent)]
322 #[cfg_attr(feature = "miette", diagnostic(transparent))]
323 PubSpec(#[from] pubspec::Error),
324 #[error(transparent)]
325 #[cfg_attr(feature = "miette", diagnostic(transparent))]
326 Gleam(#[from] gleam::Error),
327 #[error(transparent)]
328 #[cfg_attr(feature = "miette", diagnostic(transparent))]
329 GoMod(#[from] go_mod::Error),
330 #[error(transparent)]
331 #[cfg_attr(feature = "miette", diagnostic(transparent))]
332 PackageJson(#[from] package_json::Error),
333 #[error(transparent)]
334 #[cfg_attr(feature = "miette", diagnostic(transparent))]
335 PackageLockJson(#[from] package_lock_json::Error),
336 #[error(transparent)]
337 #[cfg_attr(feature = "miette", diagnostic(transparent))]
338 MavenPom(#[from] maven_pom::Error),
339 #[error(transparent)]
340 #[cfg_attr(feature = "miette", diagnostic(transparent))]
341 TauriConfJson(#[from] tauri_conf_json::Error),
342 #[error(transparent)]
343 #[cfg_attr(feature = "miette", diagnostic(transparent))]
344 DenoLock(#[from] deno_lock::Error),
345 #[error(transparent)]
346 #[cfg_attr(feature = "miette", diagnostic(transparent))]
347 RegexFile(#[from] regex_file::Error),
348}
349
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
354pub(crate) enum Format {
355 Cargo,
356 CargoLock,
357 DenoJson,
358 PyProject,
359 PubSpec,
360 Gleam,
361 GoMod,
362 PackageJson,
363 PackageLockJson,
364 DenoLock,
365 MavenPom,
366 TauriConf,
367 RegexFile,
368}
369
370impl Format {
371 const FILE_NAMES: &'static [(&'static str, Self)] = &[
373 ("Cargo.toml", Format::Cargo),
374 ("Cargo.lock", Format::CargoLock),
375 ("deno.json", Format::DenoJson),
376 ("deno.lock", Format::DenoLock),
377 ("gleam.toml", Format::Gleam),
378 ("go.mod", Format::GoMod),
379 ("package.json", Format::PackageJson),
380 ("package-lock.json", Format::PackageLockJson),
381 ("pom.xml", Format::MavenPom),
382 ("pubspec.yaml", Format::PubSpec),
383 ("pyproject.toml", Format::PyProject),
384 ("tauri.conf.json", Format::TauriConf),
385 ("tauri.macos.conf.json", Format::TauriConf),
386 ("tauri.windows.conf.json", Format::TauriConf),
387 ("tauri.linux.conf.json", Format::TauriConf),
388 ];
389
390 fn try_from(file_name: &str) -> Option<Self> {
391 Self::FILE_NAMES
392 .iter()
393 .find(|(name, _)| file_name == *name)
394 .map(|(_, format)| *format)
395 }
396}
397
398#[derive(Debug, thiserror::Error)]
399#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
400pub enum ConfigError {
401 #[error(transparent)]
402 #[cfg_attr(feature = "miette", diagnostic(transparent))]
403 UnknownFile(#[from] UnknownFile),
404 #[error(transparent)]
405 #[cfg_attr(feature = "miette", diagnostic(transparent))]
406 ConflictingOptions(#[from] ConflictingOptions),
407}
408
409#[derive(Debug, thiserror::Error)]
410#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
411#[error("Unknown file: {path}")]
412#[cfg_attr(
413 feature = "miette",
414 diagnostic(
415 code(knope_versioning::versioned_file::unknown_file),
416 help("Knope identities the type of file based on its name."),
417 url("https://knope.tech/reference/config-file/packages#versioned_files")
418 )
419)]
420pub struct UnknownFile {
421 pub path: RelativePathBuf,
422}
423
424#[derive(Debug, thiserror::Error)]
425#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
426#[error("Cannot specify both 'dependency' and 'regex' for the same file: {path}")]
427#[cfg_attr(
428 feature = "miette",
429 diagnostic(
430 code(knope_versioning::versioned_file::conflicting_options),
431 help(
432 "Use 'dependency' to update a dependency version in a known file format, or 'regex' to match version strings in arbitrary text files, but not both."
433 ),
434 url("https://knope.tech/reference/config-file/packages#versioned_files")
435 )
436)]
437pub struct ConflictingOptions {
438 pub path: RelativePathBuf,
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
443pub struct Config {
444 pub(crate) path: RelativePathBuf,
446 pub(crate) format: Format,
448 pub dependency: Option<String>,
450 pub regex: Option<Vec<String>>,
452}
453
454impl Config {
455 pub fn new(
462 path: RelativePathBuf,
463 dependency: Option<String>,
464 regex: Option<Vec<String>>,
465 ) -> Result<Self, ConfigError> {
466 if dependency.is_some() && regex.is_some() {
467 return Err(ConflictingOptions { path }.into());
468 }
469
470 if regex.is_some() {
471 return Ok(Config {
472 path,
473 format: Format::RegexFile,
474 dependency,
475 regex,
476 });
477 }
478
479 let Some(file_name) = path.file_name() else {
480 return Err(UnknownFile { path }.into());
481 };
482 let Some(format) = Format::try_from(file_name) else {
483 return Err(UnknownFile { path }.into());
484 };
485 Ok(Config {
486 path,
487 format,
488 dependency,
489 regex: None,
490 })
491 }
492
493 #[must_use]
494 pub fn as_path(&self) -> RelativePathBuf {
495 self.path.clone()
496 }
497
498 #[must_use]
499 pub fn to_pathbuf(&self) -> PathBuf {
500 self.as_path().to_path("")
501 }
502
503 pub fn defaults() -> impl Iterator<Item = Self> {
504 Format::FILE_NAMES
505 .iter()
506 .copied()
507 .map(|(name, format)| Self {
508 format,
509 path: RelativePathBuf::from(name),
510 dependency: None,
511 regex: None,
512 })
513 }
514}
515
516impl Serialize for Config {
517 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
518 where
519 S: Serializer,
520 {
521 self.as_path().serialize(serializer)
522 }
523}
524
525impl From<&Config> for PathBuf {
526 fn from(path: &Config) -> Self {
527 path.as_path().to_path("")
528 }
529}
530
531impl PartialEq<RelativePathBuf> for Config {
532 fn eq(&self, other: &RelativePathBuf) -> bool {
533 self.path == *other
534 }
535}
536
537impl PartialEq<Config> for RelativePathBuf {
538 fn eq(&self, other: &Config) -> bool {
539 other == self
540 }
541}