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