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 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 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 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#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct Config {
281 parent: Option<RelativePathBuf>,
283 pub(crate) format: Format,
285 pub dependency: Option<String>,
287}
288
289impl Config {
290 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}