1use derive_builder::Builder;
12use reqwest::Method;
13use std::borrow::Cow;
14
15use crate::api::projects::ProjectEssentials;
16use crate::api::{Endpoint, ReturnsJsonResponse};
17use serde::Serialize;
18
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct VersionEssentials {
23 pub id: u64,
25 pub name: String,
27}
28
29impl From<Version> for VersionEssentials {
30 fn from(v: Version) -> Self {
31 VersionEssentials {
32 id: v.id,
33 name: v.name,
34 }
35 }
36}
37
38impl From<&Version> for VersionEssentials {
39 fn from(v: &Version) -> Self {
40 VersionEssentials {
41 id: v.id,
42 name: v.name.to_owned(),
43 }
44 }
45}
46
47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51pub struct Version {
52 pub id: u64,
54 pub name: String,
56 pub project: ProjectEssentials,
58 pub description: String,
60 pub status: VersionStatus,
62 pub due_date: Option<time::Date>,
64 pub sharing: VersionSharing,
66 #[serde(
68 serialize_with = "crate::api::serialize_rfc3339",
69 deserialize_with = "crate::api::deserialize_rfc3339"
70 )]
71 pub created_on: time::OffsetDateTime,
72 #[serde(
74 serialize_with = "crate::api::serialize_rfc3339",
75 deserialize_with = "crate::api::deserialize_rfc3339"
76 )]
77 pub updated_on: time::OffsetDateTime,
78 #[serde(default)]
80 wiki_page_title: Option<String>,
81}
82
83#[derive(Debug, Clone, Builder)]
85#[builder(setter(strip_option))]
86pub struct ListVersions<'a> {
87 #[builder(setter(into))]
89 project_id_or_name: Cow<'a, str>,
90}
91
92impl ReturnsJsonResponse for ListVersions<'_> {}
93
94impl<'a> ListVersions<'a> {
95 #[must_use]
97 pub fn builder() -> ListVersionsBuilder<'a> {
98 ListVersionsBuilder::default()
99 }
100}
101
102impl Endpoint for ListVersions<'_> {
103 fn method(&self) -> Method {
104 Method::GET
105 }
106
107 fn endpoint(&self) -> Cow<'static, str> {
108 format!("projects/{}/versions.json", self.project_id_or_name).into()
109 }
110}
111
112#[derive(Debug, Clone, Builder)]
114#[builder(setter(strip_option))]
115pub struct GetVersion {
116 id: u64,
118}
119
120impl ReturnsJsonResponse for GetVersion {}
121
122impl GetVersion {
123 #[must_use]
125 pub fn builder() -> GetVersionBuilder {
126 GetVersionBuilder::default()
127 }
128}
129
130impl Endpoint for GetVersion {
131 fn method(&self) -> Method {
132 Method::GET
133 }
134
135 fn endpoint(&self) -> Cow<'static, str> {
136 format!("versions/{}.json", &self.id).into()
137 }
138}
139
140#[derive(Debug, Clone, serde::Deserialize, Serialize)]
143#[serde(rename_all = "snake_case")]
144pub enum VersionStatus {
145 Open,
147 Locked,
149 Closed,
151}
152
153#[derive(Debug, Clone, serde::Deserialize, Serialize)]
155#[serde(rename_all = "snake_case")]
156pub enum VersionSharing {
157 None,
159 Descendants,
161 Hierarchy,
163 Tree,
165 System,
167}
168
169#[serde_with::skip_serializing_none]
171#[derive(Debug, Clone, Builder, Serialize)]
172#[builder(setter(strip_option))]
173pub struct CreateVersion<'a> {
174 #[builder(setter(into))]
176 #[serde(skip_serializing)]
177 project_id_or_name: Cow<'a, str>,
178 #[builder(setter(into))]
180 name: Cow<'a, str>,
181 #[builder(default)]
183 status: Option<VersionStatus>,
184 #[builder(default)]
186 sharing: Option<VersionSharing>,
187 #[builder(default)]
189 due_date: Option<time::Date>,
190 #[builder(default)]
192 description: Option<Cow<'a, str>>,
193 #[builder(default)]
195 wiki_page_title: Option<Cow<'a, str>>,
196}
197
198impl ReturnsJsonResponse for CreateVersion<'_> {}
199
200impl<'a> CreateVersion<'a> {
201 #[must_use]
203 pub fn builder() -> CreateVersionBuilder<'a> {
204 CreateVersionBuilder::default()
205 }
206}
207
208impl Endpoint for CreateVersion<'_> {
209 fn method(&self) -> Method {
210 Method::POST
211 }
212
213 fn endpoint(&self) -> Cow<'static, str> {
214 format!("projects/{}/versions.json", self.project_id_or_name).into()
215 }
216
217 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
218 Ok(Some((
219 "application/json",
220 serde_json::to_vec(&VersionWrapper::<CreateVersion> {
221 version: (*self).to_owned(),
222 })?,
223 )))
224 }
225}
226
227#[serde_with::skip_serializing_none]
229#[derive(Debug, Clone, Builder, Serialize)]
230#[builder(setter(strip_option))]
231pub struct UpdateVersion<'a> {
232 #[serde(skip_serializing)]
234 id: u64,
235 #[builder(default, setter(into))]
237 name: Option<Cow<'a, str>>,
238 #[builder(default)]
240 status: Option<VersionStatus>,
241 #[builder(default)]
243 sharing: Option<VersionSharing>,
244 #[builder(default)]
246 due_date: Option<time::Date>,
247 #[builder(default)]
249 description: Option<Cow<'a, str>>,
250 #[builder(default)]
252 wiki_page_title: Option<Cow<'a, str>>,
253}
254
255impl<'a> UpdateVersion<'a> {
256 #[must_use]
258 pub fn builder() -> UpdateVersionBuilder<'a> {
259 UpdateVersionBuilder::default()
260 }
261}
262
263impl Endpoint for UpdateVersion<'_> {
264 fn method(&self) -> Method {
265 Method::PUT
266 }
267
268 fn endpoint(&self) -> Cow<'static, str> {
269 format!("versions/{}.json", self.id).into()
270 }
271
272 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
273 Ok(Some((
274 "application/json",
275 serde_json::to_vec(&VersionWrapper::<UpdateVersion> {
276 version: (*self).to_owned(),
277 })?,
278 )))
279 }
280}
281
282#[derive(Debug, Clone, Builder)]
284#[builder(setter(strip_option))]
285pub struct DeleteVersion {
286 id: u64,
288}
289
290impl DeleteVersion {
291 #[must_use]
293 pub fn builder() -> DeleteVersionBuilder {
294 DeleteVersionBuilder::default()
295 }
296}
297
298impl Endpoint for DeleteVersion {
299 fn method(&self) -> Method {
300 Method::DELETE
301 }
302
303 fn endpoint(&self) -> Cow<'static, str> {
304 format!("versions/{}.json", &self.id).into()
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
310pub struct VersionsWrapper<T> {
311 pub versions: Vec<T>,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
318pub struct VersionWrapper<T> {
319 pub version: T,
321}
322
323#[cfg(test)]
324mod test {
325 use super::*;
326 use crate::api::test_helpers::with_project;
327 use pretty_assertions::assert_eq;
328 use std::error::Error;
329 use tokio::sync::RwLock;
330 use tracing_test::traced_test;
331
332 static VERSION_LOCK: RwLock<()> = RwLock::const_new(());
335
336 #[traced_test]
337 #[test]
338 fn test_list_versions_no_pagination() -> Result<(), Box<dyn Error>> {
339 let _r_versions = VERSION_LOCK.read();
340 dotenvy::dotenv()?;
341 let redmine = crate::api::Redmine::from_env()?;
342 let endpoint = ListVersions::builder().project_id_or_name("92").build()?;
343 redmine.json_response_body::<_, VersionsWrapper<Version>>(&endpoint)?;
344 Ok(())
345 }
346
347 #[traced_test]
348 #[test]
349 fn test_get_version() -> Result<(), Box<dyn Error>> {
350 let _r_versions = VERSION_LOCK.read();
351 dotenvy::dotenv()?;
352 let redmine = crate::api::Redmine::from_env()?;
353 let endpoint = GetVersion::builder().id(1182).build()?;
354 redmine.json_response_body::<_, VersionWrapper<Version>>(&endpoint)?;
355 Ok(())
356 }
357
358 #[function_name::named]
359 #[traced_test]
360 #[test]
361 fn test_create_version() -> Result<(), Box<dyn Error>> {
362 let _w_versions = VERSION_LOCK.write();
363 let name = format!("unittest_{}", function_name!());
364 with_project(&name, |redmine, _, name| {
365 let create_endpoint = CreateVersion::builder()
366 .project_id_or_name(name)
367 .name("Test Version")
368 .build()?;
369 redmine.json_response_body::<_, VersionWrapper<Version>>(&create_endpoint)?;
370 Ok(())
371 })?;
372 Ok(())
373 }
374
375 #[function_name::named]
376 #[traced_test]
377 #[test]
378 fn test_update_version() -> Result<(), Box<dyn Error>> {
379 let _w_versions = VERSION_LOCK.write();
380 let name = format!("unittest_{}", function_name!());
381 with_project(&name, |redmine, _, name| {
382 let create_endpoint = CreateVersion::builder()
383 .project_id_or_name(name)
384 .name("Test Version")
385 .build()?;
386 let VersionWrapper { version } =
387 redmine.json_response_body::<_, VersionWrapper<Version>>(&create_endpoint)?;
388 let update_endpoint = super::UpdateVersion::builder()
389 .id(version.id)
390 .name("Neue Test-Version")
391 .build()?;
392 redmine.ignore_response_body::<_>(&update_endpoint)?;
393 Ok(())
394 })?;
395 Ok(())
396 }
397
398 #[traced_test]
403 #[test]
404 fn test_completeness_version_type() -> Result<(), Box<dyn Error>> {
405 let _r_versions = VERSION_LOCK.read();
406 dotenvy::dotenv()?;
407 let redmine = crate::api::Redmine::from_env()?;
408 let endpoint = ListVersions::builder().project_id_or_name("92").build()?;
409 let VersionsWrapper { versions: values } =
410 redmine.json_response_body::<_, VersionsWrapper<serde_json::Value>>(&endpoint)?;
411 for value in values {
412 let o: Version = serde_json::from_value(value.clone())?;
413 let reserialized = serde_json::to_value(o)?;
414 assert_eq!(value, reserialized);
415 }
416 Ok(())
417 }
418}