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