redmine_api/api/
news.rs

1//! News Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_News)
4//!
5//! - [x] all news endpoint
6//! - [x] project news endpoint
7//!
8use 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/// a type for news to use as an API return type
17///
18/// alternatively you can use your own type limited to the fields you need
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct News {
21    /// numeric id
22    pub id: u64,
23    /// the project the news was published in
24    pub project: ProjectEssentials,
25    /// the author of the news
26    pub author: UserEssentials,
27    /// the title of the news
28    pub title: String,
29    /// the summary of the news
30    pub summary: String,
31    /// the description of the news (body)
32    pub description: String,
33    /// The time when this project was created
34    #[serde(
35        serialize_with = "crate::api::serialize_rfc3339",
36        deserialize_with = "crate::api::deserialize_rfc3339"
37    )]
38    pub created_on: time::OffsetDateTime,
39}
40/// The endpoint for all news
41#[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    /// Create a builder for the endpoint.
54    #[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/// The endpoint for project news
71#[derive(Debug, Clone, Builder)]
72#[builder(setter(strip_option))]
73pub struct ListProjectNews<'a> {
74    /// project id or name as it appears in the URL
75    #[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    /// Create a builder for the endpoint.
88    #[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/// helper struct for outer layers with a news field holding the inner data
105#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106pub struct NewsWrapper<T> {
107    /// to parse JSON with news key
108    pub news: Vec<T>,
109}
110
111#[cfg(test)]
112mod test {
113    use super::*;
114    use pretty_assertions::assert_eq;
115    use std::error::Error;
116    use tracing_test::traced_test;
117
118    #[traced_test]
119    #[test]
120    fn test_list_news_no_pagination() -> Result<(), Box<dyn Error>> {
121        dotenvy::dotenv()?;
122        let redmine = crate::api::Redmine::from_env()?;
123        let endpoint = ListNews::builder().build()?;
124        redmine.json_response_body::<_, NewsWrapper<News>>(&endpoint)?;
125        Ok(())
126    }
127
128    #[traced_test]
129    #[test]
130    fn test_list_news_first_page() -> Result<(), Box<dyn Error>> {
131        dotenvy::dotenv()?;
132        let redmine = crate::api::Redmine::from_env()?;
133        let endpoint = ListNews::builder().build()?;
134        redmine.json_response_body_page::<_, News>(&endpoint, 0, 25)?;
135        Ok(())
136    }
137
138    #[traced_test]
139    #[test]
140    fn test_list_news_all_pages() -> Result<(), Box<dyn Error>> {
141        dotenvy::dotenv()?;
142        let redmine = crate::api::Redmine::from_env()?;
143        let endpoint = ListNews::builder().build()?;
144        redmine.json_response_body_all_pages::<_, News>(&endpoint)?;
145        Ok(())
146    }
147
148    /// this tests if any of the results contain a field we are not deserializing
149    ///
150    /// this will only catch fields we missed if they are part of the response but
151    /// it is better than nothing
152    #[traced_test]
153    #[test]
154    fn test_completeness_news_type() -> Result<(), Box<dyn Error>> {
155        dotenvy::dotenv()?;
156        let redmine = crate::api::Redmine::from_env()?;
157        let endpoint = ListNews::builder().build()?;
158        let NewsWrapper { news: values } =
159            redmine.json_response_body::<_, NewsWrapper<serde_json::Value>>(&endpoint)?;
160        for value in values {
161            let o: News = serde_json::from_value(value.clone())?;
162            let reserialized = serde_json::to_value(o)?;
163            assert_eq!(value, reserialized);
164        }
165        Ok(())
166    }
167}