redmine_api/api/
files.rs

1//! Files Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Files)
4//!
5//! - [x] project specific files endpoint
6//! - [ ] create file endpoint
7
8use std::borrow::Cow;
9
10use derive_builder::Builder;
11use reqwest::Method;
12use serde::Serialize;
13
14use crate::api::users::UserEssentials;
15use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
16
17/// a type for files to use as an API return type
18///
19/// alternatively you can use your own type limited to the fields you need
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
21pub struct File {
22    /// numeric id
23    pub id: u64,
24    /// the filename
25    pub filename: String,
26    /// the file size in bytes
27    pub filesize: u64,
28    /// the file content type
29    pub content_type: String,
30    /// the file description
31    pub description: String,
32    /// a token for the file
33    pub token: String,
34    /// the author of the file
35    pub author: UserEssentials,
36    /// The time when this file was created
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 digest of the file
43    pub digest: String,
44    /// the number of downloads
45    pub downloads: u64,
46}
47
48/// The endpoint for all files in a Redmine project
49#[derive(Debug, Clone, Builder)]
50#[builder(setter(strip_option))]
51pub struct ListProjectFiles<'a> {
52    /// project id or name as it appears in the URL
53    #[builder(setter(into))]
54    project_id_or_name: Cow<'a, str>,
55}
56
57impl ReturnsJsonResponse for ListProjectFiles<'_> {}
58impl NoPagination for ListProjectFiles<'_> {}
59
60impl<'a> ListProjectFiles<'a> {
61    /// Create a builder for the endpoint.
62    #[must_use]
63    pub fn builder() -> ListProjectFilesBuilder<'a> {
64        ListProjectFilesBuilder::default()
65    }
66}
67
68impl Endpoint for ListProjectFiles<'_> {
69    fn method(&self) -> Method {
70        Method::GET
71    }
72
73    fn endpoint(&self) -> Cow<'static, str> {
74        format!("projects/{}/files.json", self.project_id_or_name).into()
75    }
76
77    fn parameters(&self) -> QueryParams<'_> {
78        QueryParams::default()
79    }
80}
81
82/// The endpoint to create a Redmine file
83#[derive(Debug, Clone, Builder, Serialize)]
84#[builder(setter(strip_option))]
85pub struct CreateFile<'a> {
86    /// project id or name as it appears in the URL
87    #[builder(setter(into))]
88    #[serde(skip_serializing)]
89    project_id_or_name: Cow<'a, str>,
90    /// the token of the uploaded file
91    #[builder(setter(into))]
92    token: Cow<'a, str>,
93    /// the version to attach the file to
94    #[builder(default)]
95    version_id: Option<u64>,
96    /// the filename
97    #[builder(setter(into), default)]
98    filename: Option<Cow<'a, str>>,
99    /// a description for the file
100    #[builder(setter(into), default)]
101    description: Option<Cow<'a, str>>,
102}
103
104impl<'a> CreateFile<'a> {
105    /// Create a builder for the endpoint.
106    #[must_use]
107    pub fn builder() -> CreateFileBuilder<'a> {
108        CreateFileBuilder::default()
109    }
110}
111
112impl Endpoint for CreateFile<'_> {
113    fn method(&self) -> Method {
114        Method::POST
115    }
116
117    fn endpoint(&self) -> Cow<'static, str> {
118        format!("projects/{}/files.json", self.project_id_or_name).into()
119    }
120
121    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
122        Ok(Some((
123            "application/json",
124            serde_json::to_vec(&FileWrapper::<CreateFile> {
125                file: (*self).to_owned(),
126            })?,
127        )))
128    }
129}
130
131/// helper struct for outer layers with a file field holding the inner data
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
133pub struct FileWrapper<T> {
134    /// to parse JSON with file key
135    pub file: T,
136}
137
138/// helper struct for outer layers with a files field holding the inner data
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
140pub struct FilesWrapper<T> {
141    /// to parse JSON with files key
142    pub files: Vec<T>,
143}
144
145#[cfg(test)]
146mod test {
147    use super::*;
148    use crate::api::uploads::UploadFile;
149    use pretty_assertions::assert_eq;
150    use std::error::Error;
151    use tempfile;
152    use tracing_test::traced_test;
153
154    #[function_name::named]
155    #[traced_test]
156    #[test]
157    fn test_list_project_files_no_pagination() -> Result<(), Box<dyn Error>> {
158        let name = format!("unittest_{}", function_name!());
159        crate::api::test_helpers::with_project(&name, |redmine, _id, name| {
160            let endpoint = ListProjectFiles::builder()
161                .project_id_or_name(name)
162                .build()?;
163            redmine.json_response_body::<_, FilesWrapper<File>>(&endpoint)?;
164            Ok(())
165        })?;
166        Ok(())
167    }
168
169    #[function_name::named]
170    #[traced_test]
171    #[test]
172    fn test_create_file() -> Result<(), Box<dyn Error>> {
173        let name = format!("unittest_{}", function_name!());
174        crate::api::test_helpers::with_project(&name, |redmine, _id, name| {
175            let mut temp_file = tempfile::NamedTempFile::new()?;
176            use std::io::Write;
177            write!(temp_file, "test file content")?;
178            let upload_endpoint = UploadFile::builder()
179                .file(temp_file.path().to_path_buf())
180                .content_type("text/plain")
181                .build()?;
182            let upload: crate::api::uploads::UploadWrapper<crate::api::uploads::FileUploadToken> =
183                redmine.json_response_body(&upload_endpoint)?;
184            let endpoint = CreateFile::builder()
185                .project_id_or_name(name)
186                .token(upload.upload.token)
187                .build()?;
188            redmine.ignore_response_body(&endpoint)?;
189            Ok(())
190        })?;
191        Ok(())
192    }
193
194    /// this tests if any of the results contain a field we are not deserializing
195    ///
196    /// this will only catch fields we missed if they are part of the response but
197    /// it is better than nothing
198    #[function_name::named]
199    #[traced_test]
200    #[test]
201    fn test_completeness_file_type() -> Result<(), Box<dyn Error>> {
202        let name = format!("unittest_{}", function_name!());
203        crate::api::test_helpers::with_project(&name, |redmine, _id, name| {
204            let endpoint = ListProjectFiles::builder()
205                .project_id_or_name(name)
206                .build()?;
207            let raw_values =
208                redmine.json_response_body::<_, FilesWrapper<serde_json::Value>>(&endpoint)?;
209            for value in raw_values.files {
210                let o: File = serde_json::from_value(value.clone())?;
211                let reserialized = serde_json::to_value(o)?;
212                assert_eq!(value, reserialized);
213            }
214            Ok(())
215        })?;
216        Ok(())
217    }
218}