use derive_builder::Builder;
use reqwest::Method;
use std::borrow::Cow;
use crate::api::users::UserEssentials;
use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Attachment {
pub id: u64,
pub filename: String,
pub filesize: u64,
pub content_type: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub content_url: String,
pub author: UserEssentials,
#[serde(
serialize_with = "crate::api::serialize_rfc3339",
deserialize_with = "crate::api::deserialize_rfc3339"
)]
pub created_on: time::OffsetDateTime,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub downloads: Option<u64>,
}
#[derive(Debug, Clone, Builder)]
#[builder(setter(strip_option))]
pub struct GetAttachment {
id: u64,
}
impl ReturnsJsonResponse for GetAttachment {}
impl NoPagination for GetAttachment {}
impl GetAttachment {
#[must_use]
pub fn builder() -> GetAttachmentBuilder {
GetAttachmentBuilder::default()
}
}
impl Endpoint for GetAttachment {
fn method(&self) -> Method {
Method::GET
}
fn endpoint(&self) -> Cow<'static, str> {
format!("attachments/{}.json", &self.id).into()
}
}
#[derive(Debug, Clone, Builder)]
#[builder(setter(strip_option))]
pub struct UpdateAttachment {
id: u64,
attachment: AttachmentUpdate,
}
impl UpdateAttachment {
#[must_use]
pub fn builder() -> UpdateAttachmentBuilder {
UpdateAttachmentBuilder::default()
}
}
impl ReturnsJsonResponse for UpdateAttachment {}
impl NoPagination for UpdateAttachment {}
impl Endpoint for UpdateAttachment {
fn method(&self) -> Method {
Method::PUT
}
fn endpoint(&self) -> Cow<'static, str> {
format!("attachments/{}.json", &self.id).into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
Ok(Some((
"application/json",
serde_json::to_vec(&AttachmentWrapper {
attachment: self.attachment.clone(),
})?,
)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
pub struct AttachmentUpdate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Builder)]
#[builder(setter(strip_option))]
pub struct DeleteAttachment {
id: u64,
}
impl DeleteAttachment {
#[must_use]
pub fn builder() -> DeleteAttachmentBuilder {
DeleteAttachmentBuilder::default()
}
}
impl Endpoint for DeleteAttachment {
fn method(&self) -> Method {
Method::DELETE
}
fn endpoint(&self) -> Cow<'static, str> {
format!("attachments/{}.json", &self.id).into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AttachmentWrapper<T> {
pub attachment: T,
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::error::Error;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn test_get_attachment() -> Result<(), Box<dyn Error>> {
use crate::api::issues::{CreateIssue, GetIssue, Issue, IssueWrapper, UploadedAttachment};
use crate::api::test_helpers::with_project;
use crate::api::uploads::{FileUploadToken, UploadFile, UploadWrapper};
with_project("test_get_attachment", |redmine, project_id, _| {
let upload_endpoint = UploadFile::builder().file("README.md").build()?;
let UploadWrapper {
upload: FileUploadToken { id: _, token },
} = redmine
.json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
let create_issue_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Attachment Test Issue")
.uploads(vec![UploadedAttachment {
token: token.into(),
filename: "README.md".into(),
description: Some("Uploaded as part of unit test for redmine-api".into()),
content_type: "application/octet-stream".into(),
}])
.build()?;
let IssueWrapper {
issue: created_issue,
} = redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_issue_endpoint)?;
let get_issue_endpoint = GetIssue::builder()
.id(created_issue.id)
.include(vec![crate::api::issues::IssueInclude::Attachments])
.build()?;
let IssueWrapper { issue } =
redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
let attachment_id = issue.attachments.unwrap().first().unwrap().id;
let endpoint = GetAttachment::builder().id(attachment_id).build()?;
redmine.json_response_body::<_, AttachmentWrapper<Attachment>>(&endpoint)?;
Ok(())
})
}
#[traced_test]
#[test]
fn test_completeness_attachment_type() -> Result<(), Box<dyn Error>> {
dotenvy::dotenv()?;
let redmine = crate::api::Redmine::from_env(
reqwest::blocking::Client::builder()
.tls_backend_rustls()
.build()?,
)?;
let endpoint = GetAttachment::builder().id(38468).build()?;
let AttachmentWrapper { attachment: value } =
redmine.json_response_body::<_, AttachmentWrapper<serde_json::Value>>(&endpoint)?;
let o: Attachment = serde_json::from_value(value.clone())?;
let reserialized = serde_json::to_value(o)?;
assert_eq!(value, reserialized);
Ok(())
}
#[traced_test]
#[test]
fn test_update_delete_attachment() -> Result<(), Box<dyn Error>> {
use crate::api::issues::{CreateIssue, GetIssue, Issue, IssueWrapper, UploadedAttachment};
use crate::api::test_helpers::with_project;
use crate::api::uploads::{FileUploadToken, UploadFile, UploadWrapper};
with_project("test_update_delete_attachment", |redmine, project_id, _| {
let upload_endpoint = UploadFile::builder().file("README.md").build()?;
let UploadWrapper {
upload: FileUploadToken { id: _, token },
} = redmine
.json_response_body::<_, UploadWrapper<FileUploadToken>>(&upload_endpoint)?;
let create_issue_endpoint = CreateIssue::builder()
.project_id(project_id)
.subject("Attachment Test Issue")
.uploads(vec![UploadedAttachment {
token: token.into(),
filename: "README.md".into(),
description: Some("Uploaded as part of unit test for redmine-api".into()),
content_type: "application/octet-stream".into(),
}])
.build()?;
let IssueWrapper {
issue: created_issue,
} = redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_issue_endpoint)?;
let get_issue_endpoint = GetIssue::builder()
.id(created_issue.id)
.include(vec![crate::api::issues::IssueInclude::Attachments])
.build()?;
let IssueWrapper { issue } =
redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
let attachment_id = issue.attachments.unwrap().first().unwrap().id;
let update_endpoint = UpdateAttachment::builder()
.id(attachment_id)
.attachment(AttachmentUpdate {
filename: Some("new_readme.md".to_string()),
description: Some("new description".to_string()),
})
.build()?;
redmine.ignore_response_body(&update_endpoint)?;
let get_endpoint = GetAttachment::builder().id(attachment_id).build()?;
let AttachmentWrapper { attachment } =
redmine.json_response_body::<_, AttachmentWrapper<Attachment>>(&get_endpoint)?;
assert_eq!(attachment.filename, "new_readme.md");
assert_eq!(attachment.description.unwrap(), "new description");
let delete_endpoint = DeleteAttachment::builder().id(attachment_id).build()?;
redmine.ignore_response_body(&delete_endpoint)?;
let get_issue_endpoint = GetIssue::builder()
.id(issue.id)
.include(vec![crate::api::issues::IssueInclude::Attachments])
.build()?;
let IssueWrapper { issue } =
redmine.json_response_body::<_, IssueWrapper<Issue>>(&get_issue_endpoint)?;
assert!(issue.attachments.is_none_or(|v| v.is_empty()));
Ok(())
})
}
}