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