knope_versioning/versioned_file/
mod.rs

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