redmine_api/api/
attachments.rs

1//! Attachments Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Attachments)
4//!
5//! - [x] specific attachment endpoint
6//! - [ ] update attachment endpoint (not documented and the link to the issue in the wiki points to an issue about something else)
7//! - [x] delete attachment endpoint
8
9use derive_builder::Builder;
10use reqwest::Method;
11use std::borrow::Cow;
12
13use crate::api::users::UserEssentials;
14use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
15
16/// a type for attachment 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 Attachment {
21    /// numeric id
22    pub id: u64,
23    /// filename as specified on upload
24    pub filename: String,
25    /// file size
26    pub filesize: u64,
27    /// content MIME type
28    pub content_type: Option<String>,
29    /// description
30    #[serde(default)]
31    pub description: Option<String>,
32    /// url where the content of this attachment can be downloaded
33    pub content_url: String,
34    /// uploader
35    pub author: UserEssentials,
36    /// The time when this file was uploaded
37    #[serde(
38        serialize_with = "crate::api::serialize_rfc3339",
39        deserialize_with = "crate::api::deserialize_rfc3339"
40    )]
41    pub created_on: time::OffsetDateTime,
42    /// the URL for the thumbnail for this attachment
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    thumbnail_url: Option<String>,
45}
46
47/// The endpoint for a specific Redmine attachment
48#[derive(Debug, Clone, Builder)]
49#[builder(setter(strip_option))]
50pub struct GetAttachment {
51    /// id of the attachment to retrieve
52    id: u64,
53}
54
55impl ReturnsJsonResponse for GetAttachment {}
56impl NoPagination for GetAttachment {}
57
58impl GetAttachment {
59    /// Create a builder for the endpoint.
60    #[must_use]
61    pub fn builder() -> GetAttachmentBuilder {
62        GetAttachmentBuilder::default()
63    }
64}
65
66impl Endpoint for GetAttachment {
67    fn method(&self) -> Method {
68        Method::GET
69    }
70
71    fn endpoint(&self) -> Cow<'static, str> {
72        format!("attachments/{}.json", &self.id).into()
73    }
74}
75
76/// The endpoint to delete a Redmine attachment
77#[derive(Debug, Clone, Builder)]
78#[builder(setter(strip_option))]
79pub struct DeleteAttachment {
80    /// id of the attachment to delete
81    id: u64,
82}
83
84impl DeleteAttachment {
85    /// Create a builder for the endpoint.
86    #[must_use]
87    pub fn builder() -> DeleteAttachmentBuilder {
88        DeleteAttachmentBuilder::default()
89    }
90}
91
92impl Endpoint for DeleteAttachment {
93    fn method(&self) -> Method {
94        Method::DELETE
95    }
96
97    fn endpoint(&self) -> Cow<'static, str> {
98        format!("attachments/{}.json", &self.id).into()
99    }
100}
101
102/// helper struct for outer layers with a attachment field holding the inner data
103#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
104pub struct AttachmentWrapper<T> {
105    /// to parse JSON with attachment key
106    pub attachment: T,
107}
108
109#[cfg(test)]
110mod test {
111    use super::*;
112    use pretty_assertions::assert_eq;
113    use std::error::Error;
114    use tracing_test::traced_test;
115
116    #[traced_test]
117    #[test]
118    fn test_get_attachment() -> Result<(), Box<dyn Error>> {
119        dotenvy::dotenv()?;
120        let redmine = crate::api::Redmine::from_env(
121            reqwest::blocking::Client::builder()
122                .use_rustls_tls()
123                .build()?,
124        )?;
125        let endpoint = GetAttachment::builder().id(38468).build()?;
126        redmine.json_response_body::<_, AttachmentWrapper<Attachment>>(&endpoint)?;
127        Ok(())
128    }
129
130    /// this tests if any of the results contain a field we are not deserializing
131    ///
132    /// this will only catch fields we missed if they are part of the response but
133    /// it is better than nothing
134    #[traced_test]
135    #[test]
136    fn test_completeness_attachment_type() -> Result<(), Box<dyn Error>> {
137        dotenvy::dotenv()?;
138        let redmine = crate::api::Redmine::from_env(
139            reqwest::blocking::Client::builder()
140                .use_rustls_tls()
141                .build()?,
142        )?;
143        let endpoint = GetAttachment::builder().id(38468).build()?;
144        let AttachmentWrapper { attachment: value } =
145            redmine.json_response_body::<_, AttachmentWrapper<serde_json::Value>>(&endpoint)?;
146        let o: Attachment = serde_json::from_value(value.clone())?;
147        let reserialized = serde_json::to_value(o)?;
148        assert_eq!(value, reserialized);
149        Ok(())
150    }
151}