1use derive_builder::Builder;
10use reqwest::Method;
11use std::borrow::Cow;
12
13use crate::api::users::UserEssentials;
14use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
15
16#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub struct Attachment {
21 pub id: u64,
23 pub filename: String,
25 pub filesize: u64,
27 pub content_type: Option<String>,
29 #[serde(default)]
31 pub description: Option<String>,
32 pub content_url: String,
34 pub author: UserEssentials,
36 #[serde(
38 serialize_with = "crate::api::serialize_rfc3339",
39 deserialize_with = "crate::api::deserialize_rfc3339"
40 )]
41 pub created_on: time::OffsetDateTime,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub thumbnail_url: Option<String>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub digest: Option<String>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub downloads: Option<u64>,
51}
52
53#[derive(Debug, Clone, Builder)]
55#[builder(setter(strip_option))]
56pub struct GetAttachment {
57 id: u64,
59}
60
61impl ReturnsJsonResponse for GetAttachment {}
62impl NoPagination for GetAttachment {}
63
64impl GetAttachment {
65 #[must_use]
67 pub fn builder() -> GetAttachmentBuilder {
68 GetAttachmentBuilder::default()
69 }
70}
71
72impl Endpoint for GetAttachment {
73 fn method(&self) -> Method {
74 Method::GET
75 }
76
77 fn endpoint(&self) -> Cow<'static, str> {
78 format!("attachments/{}.json", &self.id).into()
79 }
80}
81
82#[derive(Debug, Clone, Builder)]
84#[builder(setter(strip_option))]
85pub struct UpdateAttachment {
86 id: u64,
88 attachment: AttachmentUpdate,
90}
91
92impl UpdateAttachment {
93 #[must_use]
95 pub fn builder() -> UpdateAttachmentBuilder {
96 UpdateAttachmentBuilder::default()
97 }
98}
99
100impl ReturnsJsonResponse for UpdateAttachment {}
101impl NoPagination for UpdateAttachment {}
102
103impl Endpoint for UpdateAttachment {
104 fn method(&self) -> Method {
105 Method::PUT
106 }
107
108 fn endpoint(&self) -> Cow<'static, str> {
109 format!("attachments/{}.json", &self.id).into()
110 }
111
112 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
113 Ok(Some((
114 "application/json",
115 serde_json::to_vec(&AttachmentWrapper {
116 attachment: self.attachment.clone(),
117 })?,
118 )))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
124pub struct AttachmentUpdate {
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub filename: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub description: Option<String>,
131}
132
133#[derive(Debug, Clone, Builder)]
135#[builder(setter(strip_option))]
136pub struct DeleteAttachment {
137 id: u64,
139}
140
141impl DeleteAttachment {
142 #[must_use]
144 pub fn builder() -> DeleteAttachmentBuilder {
145 DeleteAttachmentBuilder::default()
146 }
147}
148
149impl Endpoint for DeleteAttachment {
150 fn method(&self) -> Method {
151 Method::DELETE
152 }
153
154 fn endpoint(&self) -> Cow<'static, str> {
155 format!("attachments/{}.json", &self.id).into()
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
161pub struct AttachmentWrapper<T> {
162 pub attachment: T,
164}
165
166#[cfg(test)]
167mod test {
168 use super::*;
169 use pretty_assertions::assert_eq;
170 use std::error::Error;
171 use tracing_test::traced_test;
172
173 #[traced_test]
174 #[test]
175 fn test_get_attachment() -> Result<(), Box<dyn Error>> {
176 use crate::api::issues::{CreateIssue, GetIssue, Issue, IssueWrapper, UploadedAttachment};
177 use crate::api::test_helpers::with_project;
178 use crate::api::uploads::{FileUploadToken, UploadFile, UploadWrapper};
179
180 with_project("test_get_attachment", |redmine, project_id, _| {
181 let upload_endpoint = UploadFile::builder().file("README.md").build()?;
182 let UploadWrapper {
183 upload: FileUploadToken { id: _, token },
184 } = redmine
185 .json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
186 let create_issue_endpoint = CreateIssue::builder()
187 .project_id(project_id)
188 .subject("Attachment Test Issue")
189 .uploads(vec![UploadedAttachment {
190 token: token.into(),
191 filename: "README.md".into(),
192 description: Some("Uploaded as part of unit test for redmine-api".into()),
193 content_type: "application/octet-stream".into(),
194 }])
195 .build()?;
196 let IssueWrapper {
197 issue: created_issue,
198 } = redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_issue_endpoint)?;
199
200 let get_issue_endpoint = GetIssue::builder()
201 .id(created_issue.id)
202 .include(vec![crate::api::issues::IssueInclude::Attachments])
203 .build()?;
204 let IssueWrapper { issue } =
205 redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
206 let attachment_id = issue.attachments.unwrap().first().unwrap().id;
207
208 let endpoint = GetAttachment::builder().id(attachment_id).build()?;
209 redmine.json_response_body::<_, AttachmentWrapper<Attachment>>(&endpoint)?;
210 Ok(())
211 })
212 }
213
214 #[traced_test]
219 #[test]
220 fn test_completeness_attachment_type() -> Result<(), Box<dyn Error>> {
221 dotenvy::dotenv()?;
222 let redmine = crate::api::Redmine::from_env(
223 reqwest::blocking::Client::builder()
224 .use_rustls_tls()
225 .build()?,
226 )?;
227 let endpoint = GetAttachment::builder().id(38468).build()?;
228 let AttachmentWrapper { attachment: value } =
229 redmine.json_response_body::<_, AttachmentWrapper<serde_json::Value>>(&endpoint)?;
230 let o: Attachment = serde_json::from_value(value.clone())?;
231 let reserialized = serde_json::to_value(o)?;
232 assert_eq!(value, reserialized);
233 Ok(())
234 }
235
236 #[traced_test]
237 #[test]
238 fn test_update_delete_attachment() -> Result<(), Box<dyn Error>> {
239 use crate::api::issues::{CreateIssue, GetIssue, Issue, IssueWrapper, UploadedAttachment};
240 use crate::api::test_helpers::with_project;
241 use crate::api::uploads::{FileUploadToken, UploadFile, UploadWrapper};
242
243 with_project("test_update_delete_attachment", |redmine, project_id, _| {
244 let upload_endpoint = UploadFile::builder().file("README.md").build()?;
245 let UploadWrapper {
246 upload: FileUploadToken { id: _, token },
247 } = redmine
248 .json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
249 let create_issue_endpoint = CreateIssue::builder()
250 .project_id(project_id)
251 .subject("Attachment Test Issue")
252 .uploads(vec![UploadedAttachment {
253 token: token.into(),
254 filename: "README.md".into(),
255 description: Some("Uploaded as part of unit test for redmine-api".into()),
256 content_type: "application/octet-stream".into(),
257 }])
258 .build()?;
259 let IssueWrapper {
260 issue: created_issue,
261 } = redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_issue_endpoint)?;
262
263 let get_issue_endpoint = GetIssue::builder()
264 .id(created_issue.id)
265 .include(vec![crate::api::issues::IssueInclude::Attachments])
266 .build()?;
267 let IssueWrapper { issue } =
268 redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
269 let attachment_id = issue.attachments.unwrap().first().unwrap().id;
270
271 let update_endpoint = UpdateAttachment::builder()
272 .id(attachment_id)
273 .attachment(AttachmentUpdate {
274 filename: Some("new_readme.md".to_string()),
275 description: Some("new description".to_string()),
276 })
277 .build()?;
278 redmine.ignore_response_body(&update_endpoint)?;
279
280 let get_endpoint = GetAttachment::builder().id(attachment_id).build()?;
281 let AttachmentWrapper { attachment } =
282 redmine.json_response_body::<_, AttachmentWrapper<Attachment>>(&get_endpoint)?;
283 assert_eq!(attachment.filename, "new_readme.md");
284 assert_eq!(attachment.description.unwrap(), "new description");
285
286 let delete_endpoint = DeleteAttachment::builder().id(attachment_id).build()?;
287 redmine.ignore_response_body(&delete_endpoint)?;
288
289 let get_issue_endpoint = GetIssue::builder()
290 .id(issue.id)
291 .include(vec![crate::api::issues::IssueInclude::Attachments])
292 .build()?;
293 let IssueWrapper { issue } =
294 redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
295 assert!(issue.attachments.is_none_or(|v| v.is_empty()));
296 Ok(())
297 })
298 }
299}