1use derive_builder::Builder;
9use reqwest::Method;
10use std::borrow::Cow;
11
12use crate::api::projects::ProjectEssentials;
13use crate::api::users::UserEssentials;
14use crate::api::{Endpoint, Pageable, ReturnsJsonResponse};
15
16#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct News {
21 pub id: u64,
23 pub project: ProjectEssentials,
25 pub author: UserEssentials,
27 pub title: String,
29 pub summary: String,
31 pub description: String,
33 #[serde(
35 serialize_with = "crate::api::serialize_rfc3339",
36 deserialize_with = "crate::api::deserialize_rfc3339"
37 )]
38 pub created_on: time::OffsetDateTime,
39}
40#[derive(Debug, Clone, Builder)]
42#[builder(setter(strip_option))]
43#[expect(
44 clippy::empty_structs_with_brackets,
45 reason = "derive_builder requires named-field syntax"
46)]
47pub struct ListNews {}
48
49impl ReturnsJsonResponse for ListNews {}
50impl Pageable for ListNews {
51 fn response_wrapper_key(&self) -> String {
52 "news".to_string()
53 }
54}
55
56impl ListNews {
57 #[must_use]
59 pub fn builder() -> ListNewsBuilder {
60 ListNewsBuilder::default()
61 }
62}
63
64impl Endpoint for ListNews {
65 fn method(&self) -> Method {
66 Method::GET
67 }
68
69 fn endpoint(&self) -> Cow<'static, str> {
70 "news.json".into()
71 }
72}
73
74#[derive(Debug, Clone, Builder)]
76#[builder(setter(strip_option))]
77pub struct ListProjectNews<'a> {
78 #[builder(setter(into))]
80 project_id_or_name: Cow<'a, str>,
81}
82
83impl ReturnsJsonResponse for ListProjectNews<'_> {}
84impl Pageable for ListProjectNews<'_> {
85 fn response_wrapper_key(&self) -> String {
86 "news".to_string()
87 }
88}
89
90impl<'a> ListProjectNews<'a> {
91 #[must_use]
93 pub fn builder() -> ListProjectNewsBuilder<'a> {
94 ListProjectNewsBuilder::default()
95 }
96}
97
98impl Endpoint for ListProjectNews<'_> {
99 fn method(&self) -> Method {
100 Method::GET
101 }
102
103 fn endpoint(&self) -> Cow<'static, str> {
104 format!("projects/{}/news.json", self.project_id_or_name).into()
105 }
106}
107
108#[derive(Debug, Clone, Builder)]
110#[builder(setter(strip_option))]
111pub struct GetNews {
112 id: u64,
114}
115
116impl ReturnsJsonResponse for GetNews {}
117impl crate::api::NoPagination for GetNews {}
118
119impl GetNews {
120 #[must_use]
122 pub fn builder() -> GetNewsBuilder {
123 GetNewsBuilder::default()
124 }
125}
126
127impl Endpoint for GetNews {
128 fn method(&self) -> Method {
129 Method::GET
130 }
131
132 fn endpoint(&self) -> Cow<'static, str> {
133 format!("news/{}.json", self.id).into()
134 }
135}
136
137#[derive(Debug, Clone, Builder, serde::Serialize)]
139#[builder(setter(strip_option))]
140pub struct CreateNews<'a> {
141 #[builder(setter(into))]
143 #[serde(skip_serializing)]
144 project_id_or_name: Cow<'a, str>,
145 #[builder(setter(into))]
147 title: Cow<'a, str>,
148 #[builder(setter(into), default)]
150 summary: Option<Cow<'a, str>>,
151 #[builder(setter(into))]
153 description: Cow<'a, str>,
154}
155
156impl<'a> CreateNews<'a> {
157 #[must_use]
159 pub fn builder() -> CreateNewsBuilder<'a> {
160 CreateNewsBuilder::default()
161 }
162}
163
164impl crate::api::NoPagination for CreateNews<'_> {}
165
166impl Endpoint for CreateNews<'_> {
167 fn method(&self) -> Method {
168 Method::POST
169 }
170
171 fn endpoint(&self) -> Cow<'static, str> {
172 format!("projects/{}/news.json", self.project_id_or_name).into()
173 }
174
175 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
176 Ok(Some((
177 "application/json",
178 serde_json::to_vec(&SingleNewsWrapper::<CreateNews> {
179 news: (*self).to_owned(),
180 })?,
181 )))
182 }
183}
184
185#[derive(Debug, Clone, Builder, serde::Serialize)]
187#[builder(setter(strip_option))]
188pub struct UpdateNews<'a> {
189 #[serde(skip_serializing)]
191 id: u64,
192 #[builder(setter(into), default)]
194 #[serde(skip_serializing_if = "Option::is_none")]
195 title: Option<Cow<'a, str>>,
196 #[builder(setter(into), default)]
198 #[serde(skip_serializing_if = "Option::is_none")]
199 summary: Option<Cow<'a, str>>,
200 #[builder(setter(into), default)]
202 #[serde(skip_serializing_if = "Option::is_none")]
203 description: Option<Cow<'a, str>>,
204}
205
206impl<'a> UpdateNews<'a> {
207 #[must_use]
209 pub fn builder() -> UpdateNewsBuilder<'a> {
210 UpdateNewsBuilder::default()
211 }
212}
213
214impl Endpoint for UpdateNews<'_> {
215 fn method(&self) -> Method {
216 Method::PUT
217 }
218
219 fn endpoint(&self) -> Cow<'static, str> {
220 format!("news/{}.json", self.id).into()
221 }
222
223 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
224 Ok(Some((
225 "application/json",
226 serde_json::to_vec(&SingleNewsWrapper::<UpdateNews> {
227 news: (*self).to_owned(),
228 })?,
229 )))
230 }
231}
232
233#[derive(Debug, Clone, Builder)]
235#[builder(setter(strip_option))]
236pub struct DeleteNews {
237 id: u64,
239}
240
241impl DeleteNews {
242 #[must_use]
244 pub fn builder() -> DeleteNewsBuilder {
245 DeleteNewsBuilder::default()
246 }
247}
248
249impl Endpoint for DeleteNews {
250 fn method(&self) -> Method {
251 Method::DELETE
252 }
253
254 fn endpoint(&self) -> Cow<'static, str> {
255 format!("news/{}.json", self.id).into()
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
261pub struct NewsWrapper<T> {
262 pub news: Vec<T>,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
269pub struct SingleNewsWrapper<T> {
270 pub news: T,
272}
273
274#[cfg(test)]
275mod test {
276 use super::*;
277 use pretty_assertions::assert_eq;
278 use std::error::Error;
279 use tracing_test::traced_test;
280
281 #[traced_test]
282 #[test]
283 fn test_list_news_first_page() -> Result<(), Box<dyn Error>> {
284 dotenvy::dotenv()?;
285 let redmine = crate::api::Redmine::from_env(
286 reqwest::blocking::Client::builder()
287 .tls_backend_rustls()
288 .build()?,
289 )?;
290 let endpoint = ListNews::builder().build()?;
291 redmine.json_response_body_page::<_, News>(&endpoint, 0, 25)?;
292 Ok(())
293 }
294
295 #[traced_test]
296 #[test]
297 fn test_list_news_all_pages() -> Result<(), Box<dyn Error>> {
298 dotenvy::dotenv()?;
299 let redmine = crate::api::Redmine::from_env(
300 reqwest::blocking::Client::builder()
301 .tls_backend_rustls()
302 .build()?,
303 )?;
304 let endpoint = ListNews::builder().build()?;
305 redmine.json_response_body_all_pages::<_, News>(&endpoint)?;
306 Ok(())
307 }
308
309 #[traced_test]
310 #[test]
311 fn test_get_update_delete_news() -> Result<(), Box<dyn Error>> {
312 crate::api::test_helpers::with_project("test_get_update_delete_news", |redmine, _, name| {
313 let create_endpoint = CreateNews::builder()
314 .project_id_or_name(name)
315 .title("Test News")
316 .summary("Test Summary")
317 .description("Test Description")
318 .build()?;
319 redmine.ignore_response_body(&create_endpoint)?;
320 let list_endpoint = ListProjectNews::builder()
321 .project_id_or_name(name)
322 .build()?;
323 let news: Vec<News> = redmine.json_response_body_all_pages(&list_endpoint)?;
324 let created_news = news
325 .into_iter()
326 .find(|n| n.title == "Test News")
327 .ok_or("Could not find created news")?;
328 let get_endpoint = GetNews::builder().id(created_news.id).build()?;
329 let fetched_news: SingleNewsWrapper<News> =
330 redmine.json_response_body(&get_endpoint)?;
331 assert_eq!(created_news, fetched_news.news);
332 let update_endpoint = UpdateNews::builder()
333 .id(created_news.id)
334 .title("New Test News")
335 .build()?;
336 redmine.ignore_response_body(&update_endpoint)?;
337 let delete_endpoint = DeleteNews::builder().id(created_news.id).build()?;
338 redmine.ignore_response_body(&delete_endpoint)?;
339 Ok(())
340 })
341 }
342
343 #[traced_test]
348 #[test]
349 fn test_completeness_news_type() -> Result<(), Box<dyn Error>> {
350 dotenvy::dotenv()?;
351 let redmine = crate::api::Redmine::from_env(
352 reqwest::blocking::Client::builder()
353 .tls_backend_rustls()
354 .build()?,
355 )?;
356 let endpoint = ListNews::builder().build()?;
357 let values: Vec<serde_json::Value> = redmine.json_response_body_all_pages(&endpoint)?;
358 for value in values {
359 let o: News = serde_json::from_value(value.clone())?;
360 let reserialized = serde_json::to_value(o)?;
361 assert_eq!(value, reserialized);
362 }
363 Ok(())
364 }
365}