Skip to main content

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 tracing_test::traced_test;
152
153    #[function_name::named]
154    #[traced_test]
155    #[test]
156    fn test_list_project_files_no_pagination() -> Result<(), Box<dyn Error>> {
157        let name = format!("unittest_{}", function_name!());
158        crate::api::test_helpers::with_project(&name, |redmine, _id, name| {
159            let endpoint = ListProjectFiles::builder()
160                .project_id_or_name(name)
161                .build()?;
162            redmine.json_response_body::<_, FilesWrapper<File>>(&endpoint)?;
163            Ok(())
164        })?;
165        Ok(())
166    }
167
168    #[function_name::named]
169    #[traced_test]
170    #[test]
171    fn test_create_file() -> Result<(), Box<dyn Error>> {
172        use std::io::Write as _;
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            write!(temp_file, "test file content")?;
177            let upload_endpoint = UploadFile::builder()
178                .file(temp_file.path().to_path_buf())
179                .content_type("text/plain")
180                .build()?;
181            let upload: crate::api::uploads::UploadWrapper<crate::api::uploads::FileUploadToken> =
182                redmine.json_response_body(&upload_endpoint)?;
183            let endpoint = CreateFile::builder()
184                .project_id_or_name(name)
185                .token(upload.upload.token)
186                .build()?;
187            redmine.ignore_response_body(&endpoint)?;
188            Ok(())
189        })?;
190        Ok(())
191    }
192
193    /// this tests if any of the results contain a field we are not deserializing
194    ///
195    /// this will only catch fields we missed if they are part of the response but
196    /// it is better than nothing
197    #[function_name::named]
198    #[traced_test]
199    #[test]
200    fn test_completeness_file_type() -> Result<(), Box<dyn Error>> {
201        let name = format!("unittest_{}", function_name!());
202        crate::api::test_helpers::with_project(&name, |redmine, _id, name| {
203            let endpoint = ListProjectFiles::builder()
204                .project_id_or_name(name)
205                .build()?;
206            let raw_values =
207                redmine.json_response_body::<_, FilesWrapper<serde_json::Value>>(&endpoint)?;
208            for value in raw_values.files {
209                let o: File = serde_json::from_value(value.clone())?;
210                let reserialized = serde_json::to_value(o)?;
211                assert_eq!(value, reserialized);
212            }
213            Ok(())
214        })?;
215        Ok(())
216    }
217}