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