knope_versioning/versioned_file/
mod.rs

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    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 tauri_conf_json;
31
32#[derive(Clone, Debug)]
33pub enum VersionedFile {
34    Cargo(Cargo),
35    CargoLock(CargoLock),
36    DenoJson(DenoJson),
37    DenoLock(DenoLock),
38    PubSpec(PubSpec),
39    Gleam(Gleam),
40    GoMod(GoMod),
41    PackageJson(PackageJson),
42    PackageLockJson(PackageLockJson),
43    PyProject(PyProject),
44    MavenPom(MavenPom),
45    TauriConf(TauriConfJson),
46    TauriMacosConf(TauriConfJson),
47    TauriWindowsConf(TauriConfJson),
48    TauriLinuxConf(TauriConfJson),
49}
50
51impl VersionedFile {
52    /// Create a new `VersionedFile`
53    ///
54    /// # Errors
55    ///
56    /// Depends on the format.
57    /// If the content doesn't match the expected format, an error is returned.
58    pub fn new<S: AsRef<str> + Debug>(
59        config: &Config,
60        content: String,
61        git_tags: &[S],
62    ) -> Result<Self, Error> {
63        match config.format {
64            Format::Cargo => Cargo::new(config.as_path(), &content)
65                .map(VersionedFile::Cargo)
66                .map_err(Error::Cargo),
67            Format::CargoLock => CargoLock::new(config.as_path(), &content)
68                .map(VersionedFile::CargoLock)
69                .map_err(Error::CargoLock),
70            Format::DenoJson => DenoJson::new(config.as_path(), content)
71                .map(VersionedFile::DenoJson)
72                .map_err(Error::DenoJson),
73            Format::DenoLock => DenoLock::new(config.as_path(), &content)
74                .map(VersionedFile::DenoLock)
75                .map_err(Error::DenoLock),
76            Format::PyProject => PyProject::new(config.as_path(), content)
77                .map(VersionedFile::PyProject)
78                .map_err(Error::PyProject),
79            Format::PubSpec => PubSpec::new(config.as_path(), content)
80                .map(VersionedFile::PubSpec)
81                .map_err(Error::PubSpec),
82            Format::Gleam => Gleam::new(config.as_path(), &content)
83                .map(VersionedFile::Gleam)
84                .map_err(Error::Gleam),
85            Format::GoMod => GoMod::new(config.as_path(), content, git_tags)
86                .map(VersionedFile::GoMod)
87                .map_err(Error::GoMod),
88            Format::PackageJson => PackageJson::new(config.as_path(), content)
89                .map(VersionedFile::PackageJson)
90                .map_err(Error::PackageJson),
91            Format::PackageLockJson => PackageLockJson::new(config.as_path(), &content)
92                .map(VersionedFile::PackageLockJson)
93                .map_err(Error::PackageLockJson),
94            Format::MavenPom => MavenPom::new(config.as_path(), content)
95                .map(VersionedFile::MavenPom)
96                .map_err(Error::MavenPom),
97            Format::TauriConf => TauriConfJson::new(config.as_path(), content)
98                .map(VersionedFile::TauriConf)
99                .map_err(Error::TauriConfJson),
100        }
101    }
102
103    #[must_use]
104    pub fn path(&self) -> &RelativePathBuf {
105        match self {
106            VersionedFile::Cargo(cargo) => &cargo.path,
107            VersionedFile::CargoLock(cargo_lock) => &cargo_lock.path,
108            VersionedFile::DenoJson(deno_json) => deno_json.get_path(),
109            VersionedFile::DenoLock(deno_lock) => deno_lock.get_path(),
110            VersionedFile::PyProject(pyproject) => &pyproject.path,
111            VersionedFile::PubSpec(pubspec) => pubspec.get_path(),
112            VersionedFile::Gleam(gleam) => &gleam.path,
113            VersionedFile::GoMod(gomod) => gomod.get_path(),
114            VersionedFile::PackageJson(package_json) => package_json.get_path(),
115            VersionedFile::PackageLockJson(package_lock_json) => package_lock_json.get_path(),
116            VersionedFile::MavenPom(maven_pom) => &maven_pom.path,
117            VersionedFile::TauriConf(tauri_conf)
118            | VersionedFile::TauriMacosConf(tauri_conf)
119            | VersionedFile::TauriWindowsConf(tauri_conf)
120            | VersionedFile::TauriLinuxConf(tauri_conf) => tauri_conf.get_path(),
121        }
122    }
123
124    /// Get the package version from the file.
125    ///
126    /// # Errors
127    ///
128    /// If there's no package version for this type of file (e.g., lock file, dependency file).
129    pub fn version(&self) -> Result<Version, Error> {
130        match self {
131            VersionedFile::Cargo(cargo) => cargo.get_version().map_err(Error::Cargo),
132            VersionedFile::CargoLock(_) | VersionedFile::DenoLock(_) => Err(Error::NoVersion),
133            VersionedFile::DenoJson(deno_json) => deno_json.get_version().ok_or(Error::NoVersion),
134            VersionedFile::PyProject(pyproject) => Ok(pyproject.version.clone()),
135            VersionedFile::PubSpec(pubspec) => Ok(pubspec.get_version().clone()),
136            VersionedFile::Gleam(gleam) => Ok(gleam.get_version().map_err(Error::Gleam)?),
137            VersionedFile::GoMod(gomod) => Ok(gomod.get_version().clone()),
138            VersionedFile::PackageJson(package_json) => Ok(package_json.get_version().clone()),
139            VersionedFile::PackageLockJson(package_lock_json) => package_lock_json
140                .get_version()
141                .map_err(Error::PackageLockJson),
142            VersionedFile::MavenPom(maven_pom) => maven_pom.get_version().map_err(Error::MavenPom),
143            VersionedFile::TauriConf(tauri_conf)
144            | VersionedFile::TauriMacosConf(tauri_conf)
145            | VersionedFile::TauriWindowsConf(tauri_conf)
146            | VersionedFile::TauriLinuxConf(tauri_conf) => Ok(tauri_conf.get_version().clone()),
147        }
148    }
149
150    /// Set the version in the file.
151    ///
152    /// # Errors
153    ///
154    /// 1. If the file is `go.mod`, there are rules about what versions are allowed.
155    pub(crate) fn set_version(
156        self,
157        new_version: &Version,
158        dependency: Option<&str>,
159        go_versioning: GoVersioning,
160    ) -> Result<Self, SetError> {
161        match self {
162            Self::Cargo(cargo) => Ok(Self::Cargo(cargo.set_version(new_version, dependency))),
163            Self::CargoLock(cargo_lock) => cargo_lock
164                .set_version(new_version, dependency)
165                .map(Self::CargoLock)
166                .map_err(SetError::CargoLock),
167            Self::DenoJson(deno_json) => deno_json
168                .set_version(new_version, dependency)
169                .map_err(SetError::Json)
170                .map(Self::DenoJson),
171            Self::DenoLock(deno_lock) => deno_lock
172                .set_version(new_version, dependency)
173                .map_err(SetError::DenoLock)
174                .map(Self::DenoLock),
175            Self::PyProject(pyproject) => Ok(Self::PyProject(pyproject.set_version(new_version))),
176            Self::PubSpec(pubspec) => pubspec
177                .set_version(new_version)
178                .map_err(SetError::Yaml)
179                .map(Self::PubSpec),
180            Self::Gleam(gleam) => Ok(Self::Gleam(gleam.set_version(new_version))),
181            Self::GoMod(gomod) => gomod
182                .set_version(new_version.clone(), go_versioning)
183                .map_err(SetError::GoMod)
184                .map(Self::GoMod),
185            Self::PackageJson(package_json) => package_json
186                .set_version(new_version, dependency)
187                .map_err(SetError::Json)
188                .map(Self::PackageJson),
189            Self::PackageLockJson(package_lock_json) => Ok(Self::PackageLockJson(
190                package_lock_json.set_version(new_version, dependency),
191            )),
192            Self::MavenPom(maven_pom) => maven_pom
193                .set_version(new_version)
194                .map_err(SetError::MavenPom)
195                .map(Self::MavenPom),
196            Self::TauriConf(tauri_conf) => tauri_conf
197                .set_version(new_version)
198                .map_err(SetError::Json)
199                .map(Self::TauriConf),
200            Self::TauriMacosConf(tauri_conf) => tauri_conf
201                .set_version(new_version)
202                .map_err(SetError::Json)
203                .map(Self::TauriMacosConf),
204            Self::TauriWindowsConf(tauri_conf) => tauri_conf
205                .set_version(new_version)
206                .map_err(SetError::Json)
207                .map(Self::TauriWindowsConf),
208            Self::TauriLinuxConf(tauri_conf) => tauri_conf
209                .set_version(new_version)
210                .map_err(SetError::Json)
211                .map(Self::TauriLinuxConf),
212        }
213    }
214
215    pub fn write(self) -> Option<impl IntoIterator<Item = Action>> {
216        match self {
217            Self::Cargo(cargo) => cargo.write().map(Single),
218            Self::CargoLock(cargo_lock) => cargo_lock.write().map(Single),
219            Self::DenoJson(deno_json) => deno_json.write().map(Single),
220            Self::PyProject(pyproject) => pyproject.write().map(Single),
221            Self::PubSpec(pubspec) => pubspec.write().map(Single),
222            Self::Gleam(gleam) => gleam.write().map(Single),
223            Self::GoMod(gomod) => gomod.write().map(Two),
224            Self::PackageJson(package_json) => package_json.write().map(Single),
225            Self::PackageLockJson(package_lock_json) => package_lock_json.write().map(Single),
226            Self::DenoLock(deno_lock) => deno_lock.write().map(Single),
227            Self::MavenPom(maven_pom) => maven_pom.write().map(Single),
228            Self::TauriConf(tauri_conf)
229            | Self::TauriMacosConf(tauri_conf)
230            | Self::TauriWindowsConf(tauri_conf)
231            | Self::TauriLinuxConf(tauri_conf) => tauri_conf.write().map(Single),
232        }
233    }
234}
235
236#[derive(Debug, thiserror::Error)]
237#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
238pub enum SetError {
239    #[error("Error serializing JSON, this is a bug: {0}")]
240    #[cfg_attr(
241        feature = "miette",
242        diagnostic(
243            code(knope_versioning::versioned_file::json_serialize),
244            help("This is a bug in knope, please report it."),
245            url("https://github.com/knope-dev/knope/issues")
246        )
247    )]
248    Json(#[from] serde_json::Error),
249    #[error("Error serializing YAML, this is a bug: {0}")]
250    #[cfg_attr(
251        feature = "miette",
252        diagnostic(
253            code(knope_versioning::versioned_file::yaml_serialize),
254            help("This is a bug in knope, please report it."),
255            url("https://github.com/knope-dev/knope/issues"),
256        )
257    )]
258    Yaml(#[from] serde_yaml::Error),
259    #[error(transparent)]
260    #[cfg_attr(feature = "miette", diagnostic(transparent))]
261    GoMod(#[from] go_mod::SetError),
262    #[error(transparent)]
263    #[cfg_attr(feature = "miette", diagnostic(transparent))]
264    CargoLock(#[from] cargo_lock::SetError),
265    #[error(transparent)]
266    #[cfg_attr(feature = "miette", diagnostic(transparent))]
267    MavenPom(#[from] maven_pom::Error),
268    #[error(transparent)]
269    #[cfg_attr(feature = "miette", diagnostic(transparent))]
270    DenoLock(#[from] deno_lock::Error),
271}
272
273#[derive(Debug, thiserror::Error)]
274#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
275pub enum Error {
276    #[error("This file can't contain a version")]
277    #[cfg_attr(
278        feature = "miette",
279        diagnostic(
280            code(knope_versioning::versioned_file::no_version),
281            help("This is likely a bug, please report it."),
282            url("https://github.com/knope-dev/knope/issues")
283        )
284    )]
285    NoVersion,
286    #[error(transparent)]
287    #[cfg_attr(feature = "miette", diagnostic(transparent))]
288    Cargo(#[from] cargo::Error),
289    #[error(transparent)]
290    #[cfg_attr(feature = "miette", diagnostic(transparent))]
291    CargoLock(#[from] cargo_lock::Error),
292    #[error(transparent)]
293    #[cfg_attr(feature = "miette", diagnostic(transparent))]
294    DenoJson(#[from] deno_json::Error),
295    #[error(transparent)]
296    #[cfg_attr(feature = "miette", diagnostic(transparent))]
297    PyProject(#[from] pyproject::Error),
298    #[error(transparent)]
299    #[cfg_attr(feature = "miette", diagnostic(transparent))]
300    PubSpec(#[from] pubspec::Error),
301    #[error(transparent)]
302    #[cfg_attr(feature = "miette", diagnostic(transparent))]
303    Gleam(#[from] gleam::Error),
304    #[error(transparent)]
305    #[cfg_attr(feature = "miette", diagnostic(transparent))]
306    GoMod(#[from] go_mod::Error),
307    #[error(transparent)]
308    #[cfg_attr(feature = "miette", diagnostic(transparent))]
309    PackageJson(#[from] package_json::Error),
310    #[error(transparent)]
311    #[cfg_attr(feature = "miette", diagnostic(transparent))]
312    PackageLockJson(#[from] package_lock_json::Error),
313    #[error(transparent)]
314    #[cfg_attr(feature = "miette", diagnostic(transparent))]
315    MavenPom(#[from] maven_pom::Error),
316    #[error(transparent)]
317    #[cfg_attr(feature = "miette", diagnostic(transparent))]
318    TauriConfJson(#[from] tauri_conf_json::Error),
319    #[error(transparent)]
320    #[cfg_attr(feature = "miette", diagnostic(transparent))]
321    DenoLock(#[from] deno_lock::Error),
322}
323
324/// All the file types supported for versioning.
325///
326/// Be sure to add new variants to [`Format::FILE_NAMES`]
327#[derive(Clone, Copy, Debug, Eq, PartialEq)]
328pub(crate) enum Format {
329    Cargo,
330    CargoLock,
331    #[allow(dead_code)]
332    DenoJson,
333    PyProject,
334    PubSpec,
335    Gleam,
336    GoMod,
337    PackageJson,
338    PackageLockJson,
339    DenoLock,
340    MavenPom,
341    TauriConf,
342}
343
344impl Format {
345    /// This is how Knope automatically detects a file type based on its name.
346    const FILE_NAMES: &'static [(&'static str, Self)] = &[
347        ("Cargo.toml", Format::Cargo),
348        ("Cargo.lock", Format::CargoLock),
349        ("deno.json", Format::DenoJson),
350        ("deno.lock", Format::DenoLock),
351        ("gleam.toml", Format::Gleam),
352        ("go.mod", Format::GoMod),
353        ("package.json", Format::PackageJson),
354        ("package-lock.json", Format::PackageLockJson),
355        ("pom.xml", Format::MavenPom),
356        ("pubspec.yaml", Format::PubSpec),
357        ("pyproject.toml", Format::PyProject),
358        ("tauri.conf.json", Format::TauriConf),
359        ("tauri.macos.conf.json", Format::TauriConf),
360        ("tauri.windows.conf.json", Format::TauriConf),
361        ("tauri.linux.conf.json", Format::TauriConf),
362    ];
363
364    fn try_from(file_name: &str) -> Option<Self> {
365        Self::FILE_NAMES
366            .iter()
367            .find(|(name, _)| file_name == *name)
368            .map(|(_, format)| *format)
369    }
370}
371
372#[derive(Debug, thiserror::Error)]
373#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
374#[error("Unknown file: {path}")]
375#[cfg_attr(
376    feature = "miette",
377    diagnostic(
378        code(knope_versioning::versioned_file::unknown_file),
379        help("Knope identities the type of file based on its name."),
380        url("https://knope.tech/reference/config-file/packages#versioned_files")
381    )
382)]
383pub struct UnknownFile {
384    pub path: RelativePathBuf,
385}
386
387/// The configuration of a versioned file.
388#[derive(Clone, Debug, Eq, PartialEq)]
389pub struct Config {
390    /// The location of the file
391    pub(crate) path: RelativePathBuf,
392    /// The type of file
393    pub(crate) format: Format,
394    /// If, within the file, we're versioning a dependency (not the entire package)
395    pub dependency: Option<String>,
396}
397
398impl Config {
399    /// Create a verified `Config` from a `RelativePathBuf`.
400    ///
401    /// # Errors
402    ///
403    /// If the file name does not match a supported format
404    pub fn new(path: RelativePathBuf, dependency: Option<String>) -> Result<Self, UnknownFile> {
405        let Some(file_name) = path.file_name() else {
406            return Err(UnknownFile { path });
407        };
408        let Some(format) = Format::try_from(file_name) else {
409            return Err(UnknownFile { path });
410        };
411        Ok(Config {
412            path,
413            format,
414            dependency,
415        })
416    }
417
418    #[must_use]
419    pub fn as_path(&self) -> RelativePathBuf {
420        self.path.clone()
421    }
422
423    #[must_use]
424    pub fn to_pathbuf(&self) -> PathBuf {
425        self.as_path().to_path("")
426    }
427
428    pub fn defaults() -> impl Iterator<Item = Self> {
429        Format::FILE_NAMES
430            .iter()
431            .copied()
432            .map(|(name, format)| Self {
433                format,
434                path: RelativePathBuf::from(name),
435                dependency: None,
436            })
437    }
438}
439
440impl Serialize for Config {
441    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
442    where
443        S: Serializer,
444    {
445        self.as_path().serialize(serializer)
446    }
447}
448
449impl From<&Config> for PathBuf {
450    fn from(path: &Config) -> Self {
451        path.as_path().to_path("")
452    }
453}
454
455impl PartialEq<RelativePathBuf> for Config {
456    fn eq(&self, other: &RelativePathBuf) -> bool {
457        self.path == *other
458    }
459}
460
461impl PartialEq<Config> for RelativePathBuf {
462    fn eq(&self, other: &Config) -> bool {
463        other == self
464    }
465}