Skip to main content

redmine_api/api/
issue_statuses.rs

1//! Issue Statuses Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_IssueStatuses)
4//!
5//! - [x] all issue statuses endpoint
6
7use derive_builder::Builder;
8use reqwest::Method;
9use std::borrow::Cow;
10
11use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
12
13/// a minimal type for Redmine issue status used in
14/// other Redmine objects (e.g. issue)
15#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub struct IssueStatusEssentials {
17    /// numeric id
18    pub id: u64,
19    /// is this status consided closed, only included in recent Redmine versions
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub is_closed: Option<bool>,
22    /// display name
23    pub name: String,
24}
25
26impl From<IssueStatus> for IssueStatusEssentials {
27    fn from(v: IssueStatus) -> Self {
28        Self {
29            id: v.id,
30            is_closed: Some(v.is_closed),
31            name: v.name,
32        }
33    }
34}
35
36impl From<&IssueStatus> for IssueStatusEssentials {
37    fn from(v: &IssueStatus) -> Self {
38        Self {
39            id: v.id,
40            is_closed: Some(v.is_closed),
41            name: v.name.to_owned(),
42        }
43    }
44}
45
46/// a type for issue status to use as an API return type
47///
48/// alternatively you can use your own type limited to the fields you need
49#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
50pub struct IssueStatus {
51    /// numeric id
52    pub id: u64,
53    /// display name
54    pub name: String,
55    /// description
56    pub description: Option<String>,
57    /// is this status considered closed
58    pub is_closed: bool,
59}
60
61/// The endpoint for all issue statuses
62#[derive(Debug, Clone, Builder)]
63#[builder(setter(strip_option))]
64#[expect(
65    clippy::empty_structs_with_brackets,
66    reason = "derive_builder requires named-field syntax"
67)]
68pub struct ListIssueStatuses {}
69
70impl ReturnsJsonResponse for ListIssueStatuses {}
71impl NoPagination for ListIssueStatuses {}
72
73impl ListIssueStatuses {
74    /// Create a builder for the endpoint.
75    #[must_use]
76    pub fn builder() -> ListIssueStatusesBuilder {
77        ListIssueStatusesBuilder::default()
78    }
79}
80
81impl Endpoint for ListIssueStatuses {
82    fn method(&self) -> Method {
83        Method::GET
84    }
85
86    fn endpoint(&self) -> Cow<'static, str> {
87        "issue_statuses.json".into()
88    }
89}
90
91/// helper struct for outer layers with a issue_statuses field holding the inner data
92#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
93pub struct IssueStatusesWrapper<T> {
94    /// to parse JSON with issue_statuses key
95    pub issue_statuses: Vec<T>,
96}
97
98/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
99/// helper struct for outer layers with a issue_status field holding the inner data
100#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub struct IssueStatusWrapper<T> {
102    /// to parse JSON with an issue_status key
103    pub issue_status: T,
104}
105
106#[cfg(test)]
107mod test {
108    use super::*;
109    use pretty_assertions::assert_eq;
110    use std::error::Error;
111    use tracing_test::traced_test;
112
113    #[traced_test]
114    #[test]
115    fn test_list_issue_statuses_no_pagination() -> Result<(), Box<dyn Error>> {
116        dotenvy::dotenv()?;
117        let redmine = crate::api::Redmine::from_env(
118            reqwest::blocking::Client::builder()
119                .tls_backend_rustls()
120                .build()?,
121        )?;
122        let endpoint = ListIssueStatuses::builder().build()?;
123        redmine.json_response_body::<_, IssueStatusesWrapper<IssueStatus>>(&endpoint)?;
124        Ok(())
125    }
126
127    /// this tests if any of the results contain a field we are not deserializing
128    ///
129    /// this will only catch fields we missed if they are part of the response but
130    /// it is better than nothing
131    #[traced_test]
132    #[test]
133    fn test_completeness_issue_status_type() -> Result<(), Box<dyn Error>> {
134        dotenvy::dotenv()?;
135        let redmine = crate::api::Redmine::from_env(
136            reqwest::blocking::Client::builder()
137                .tls_backend_rustls()
138                .build()?,
139        )?;
140        let endpoint = ListIssueStatuses::builder().build()?;
141        let IssueStatusesWrapper {
142            issue_statuses: values,
143        } = redmine.json_response_body::<_, IssueStatusesWrapper<serde_json::Value>>(&endpoint)?;
144        for value in values {
145            let o: IssueStatus = serde_json::from_value(value.clone())?;
146            let reserialized = serde_json::to_value(o)?;
147            assert_eq!(value, reserialized);
148        }
149        Ok(())
150    }
151
152    #[test]
153    fn test_issue_status_essentials_from_issue_status() {
154        let issue_status = IssueStatus {
155            id: 1,
156            name: "New".to_string(),
157            description: Some("new issue".to_string()),
158            is_closed: false,
159        };
160        let issue_status_essentials: IssueStatusEssentials = issue_status.into();
161        assert_eq!(issue_status_essentials.id, 1);
162        assert_eq!(issue_status_essentials.name, "New");
163        assert_eq!(issue_status_essentials.is_closed, Some(false));
164    }
165}