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 regex = 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, regex)
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 #[allow(dead_code)]
358 DenoJson,
359 PyProject,
360 PubSpec,
361 Gleam,
362 GoMod,
363 PackageJson,
364 PackageLockJson,
365 DenoLock,
366 MavenPom,
367 TauriConf,
368 RegexFile,
369}
370
371impl Format {
372 const FILE_NAMES: &'static [(&'static str, Self)] = &[
374 ("Cargo.toml", Format::Cargo),
375 ("Cargo.lock", Format::CargoLock),
376 ("deno.json", Format::DenoJson),
377 ("deno.lock", Format::DenoLock),
378 ("gleam.toml", Format::Gleam),
379 ("go.mod", Format::GoMod),
380 ("package.json", Format::PackageJson),
381 ("package-lock.json", Format::PackageLockJson),
382 ("pom.xml", Format::MavenPom),
383 ("pubspec.yaml", Format::PubSpec),
384 ("pyproject.toml", Format::PyProject),
385 ("tauri.conf.json", Format::TauriConf),
386 ("tauri.macos.conf.json", Format::TauriConf),
387 ("tauri.windows.conf.json", Format::TauriConf),
388 ("tauri.linux.conf.json", Format::TauriConf),
389 ];
390
391 fn try_from(file_name: &str) -> Option<Self> {
392 Self::FILE_NAMES
393 .iter()
394 .find(|(name, _)| file_name == *name)
395 .map(|(_, format)| *format)
396 }
397}
398
399#[derive(Debug, thiserror::Error)]
400#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
401pub enum ConfigError {
402 #[error(transparent)]
403 #[cfg_attr(feature = "miette", diagnostic(transparent))]
404 UnknownFile(#[from] UnknownFile),
405 #[error(transparent)]
406 #[cfg_attr(feature = "miette", diagnostic(transparent))]
407 ConflictingOptions(#[from] ConflictingOptions),
408}
409
410#[derive(Debug, thiserror::Error)]
411#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
412#[error("Unknown file: {path}")]
413#[cfg_attr(
414 feature = "miette",
415 diagnostic(
416 code(knope_versioning::versioned_file::unknown_file),
417 help("Knope identities the type of file based on its name."),
418 url("https://knope.tech/reference/config-file/packages#versioned_files")
419 )
420)]
421pub struct UnknownFile {
422 pub path: RelativePathBuf,
423}
424
425#[derive(Debug, thiserror::Error)]
426#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
427#[error("Cannot specify both 'dependency' and 'regex' for the same file: {path}")]
428#[cfg_attr(
429 feature = "miette",
430 diagnostic(
431 code(knope_versioning::versioned_file::conflicting_options),
432 help(
433 "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."
434 ),
435 url("https://knope.tech/reference/config-file/packages#versioned_files")
436 )
437)]
438pub struct ConflictingOptions {
439 pub path: RelativePathBuf,
440}
441
442#[derive(Clone, Debug, Eq, PartialEq)]
444pub struct Config {
445 pub(crate) path: RelativePathBuf,
447 pub(crate) format: Format,
449 pub dependency: Option<String>,
451 pub regex: Option<String>,
453}
454
455impl Config {
456 pub fn new(
463 path: RelativePathBuf,
464 dependency: Option<String>,
465 regex: Option<String>,
466 ) -> Result<Self, ConfigError> {
467 if dependency.is_some() && regex.is_some() {
468 return Err(ConflictingOptions { path }.into());
469 }
470
471 if regex.is_some() {
472 return Ok(Config {
473 path,
474 format: Format::RegexFile,
475 dependency,
476 regex,
477 });
478 }
479
480 let Some(file_name) = path.file_name() else {
481 return Err(UnknownFile { path }.into());
482 };
483 let Some(format) = Format::try_from(file_name) else {
484 return Err(UnknownFile { path }.into());
485 };
486 Ok(Config {
487 path,
488 format,
489 dependency,
490 regex: None,
491 })
492 }
493
494 #[must_use]
495 pub fn as_path(&self) -> RelativePathBuf {
496 self.path.clone()
497 }
498
499 #[must_use]
500 pub fn to_pathbuf(&self) -> PathBuf {
501 self.as_path().to_path("")
502 }
503
504 pub fn defaults() -> impl Iterator<Item = Self> {
505 Format::FILE_NAMES
506 .iter()
507 .copied()
508 .map(|(name, format)| Self {
509 format,
510 path: RelativePathBuf::from(name),
511 dependency: None,
512 regex: None,
513 })
514 }
515}
516
517impl Serialize for Config {
518 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
519 where
520 S: Serializer,
521 {
522 self.as_path().serialize(serializer)
523 }
524}
525
526impl From<&Config> for PathBuf {
527 fn from(path: &Config) -> Self {
528 path.as_path().to_path("")
529 }
530}
531
532impl PartialEq<RelativePathBuf> for Config {
533 fn eq(&self, other: &RelativePathBuf) -> bool {
534 self.path == *other
535 }
536}
537
538impl PartialEq<Config> for RelativePathBuf {
539 fn eq(&self, other: &Config) -> bool {
540 other == self
541 }
542}