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