Skip to main content

axum_test/multipart/
part.rs

1use crate::internals::ErrorMessage;
2use bytes::Bytes;
3use http::HeaderName;
4use http::HeaderValue;
5use mime::Mime;
6use std::fmt::Debug;
7use std::fmt::Display;
8
9///
10/// For creating a section of a MultipartForm.
11///
12/// Use [`Part::text()`](crate::multipart::Part::text()) and [`Part::bytes()`](crate::multipart::Part::bytes()) for creating new instances.
13/// Then attach them to a `MultipartForm` using [`MultipartForm::add_part()`](crate::multipart::MultipartForm::add_part()).
14///
15#[derive(Debug, Clone)]
16pub struct Part {
17    pub(crate) bytes: Bytes,
18    pub(crate) file_name: Option<String>,
19    pub(crate) mime_type: Mime,
20    pub(crate) headers: Vec<(HeaderName, HeaderValue)>,
21}
22
23impl Part {
24    /// Creates a new part of a multipart form, that will send text.
25    ///
26    /// The default mime type for this part will be `text/plain`,
27    pub fn text<T>(text: T) -> Self
28    where
29        T: Display,
30    {
31        let bytes = text.to_string().into_bytes().into();
32
33        Self::new(bytes, mime::TEXT_PLAIN)
34    }
35
36    /// Creates a new part of a multipart form, that will upload bytes.
37    ///
38    /// The default mime type for this part will be `application/octet-stream`,
39    pub fn bytes<B>(bytes: B) -> Self
40    where
41        B: Into<Bytes>,
42    {
43        Self::new(bytes.into(), mime::APPLICATION_OCTET_STREAM)
44    }
45
46    fn new(bytes: Bytes, mime_type: Mime) -> Self {
47        Self {
48            bytes,
49            file_name: None,
50            mime_type,
51            headers: Default::default(),
52        }
53    }
54
55    /// Sets the file name for this part of a multipart form.
56    ///
57    /// By default there is no filename. This will set one.
58    pub fn file_name<T>(mut self, file_name: T) -> Self
59    where
60        T: Display,
61    {
62        self.file_name = Some(file_name.to_string());
63        self
64    }
65
66    /// Sets the mime type for this part of a multipart form.
67    ///
68    /// The default mime type is `text/plain` or `application/octet-stream`,
69    /// depending on how this instance was created.
70    /// This function will replace that.
71    pub fn mime_type<M>(mut self, mime_type: M) -> Self
72    where
73        M: AsRef<str>,
74    {
75        let raw_mime_type = mime_type.as_ref();
76        let parsed_mime_type = raw_mime_type
77            .parse()
78            .error_message_fn(|| format!("Failed to parse '{raw_mime_type}' as a Mime type"));
79
80        self.mime_type = parsed_mime_type;
81
82        self
83    }
84
85    /// Adds a header to be sent with the Part of this Multiform.
86    ///
87    /// ```rust
88    /// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
89    /// #
90    /// use axum::Router;
91    /// use axum_test::TestServer;
92    /// use axum_test::multipart::MultipartForm;
93    /// use axum_test::multipart::Part;
94    ///
95    /// let app = Router::new();
96    /// let server = TestServer::new(app);
97    ///
98    /// let readme_bytes = include_bytes!("../../README.md");
99    /// let readme_part = Part::bytes(readme_bytes.as_slice())
100    ///     .file_name(&"README.md")
101    ///     // Add a header to the Part
102    ///     .add_header("x-text-category", "readme");
103    ///
104    /// let multipart_form = MultipartForm::new()
105    ///     .add_part("file", readme_part);
106    ///
107    /// let response = server.post(&"/my-form")
108    ///     .multipart(multipart_form)
109    ///     .await;
110    /// #
111    /// # Ok(()) }
112    /// ```
113    ///
114    pub fn add_header<N, V>(mut self, name: N, value: V) -> Self
115    where
116        N: TryInto<HeaderName>,
117        N::Error: Debug,
118        V: TryInto<HeaderValue>,
119        V::Error: Debug,
120    {
121        let header_name: HeaderName = name
122            .try_into()
123            .expect("Failed to convert header name to HeaderName");
124        let header_value: HeaderValue = value
125            .try_into()
126            .expect("Failed to convert header vlue to HeaderValue");
127
128        self.headers.push((header_name, header_value));
129        self
130    }
131}
132
133#[cfg(test)]
134mod test_text {
135    use super::*;
136
137    #[test]
138    fn it_should_contain_text_given() {
139        let part = Part::text("some_text");
140
141        let output = String::from_utf8_lossy(&part.bytes);
142        assert_eq!(output, "some_text");
143    }
144
145    #[test]
146    fn it_should_use_mime_type_text() {
147        let part = Part::text("some_text");
148        assert_eq!(part.mime_type, mime::TEXT_PLAIN);
149    }
150}
151
152#[cfg(test)]
153mod test_byes {
154    use super::*;
155
156    #[test]
157    fn it_should_contain_bytes_given() {
158        let bytes = "some_text".as_bytes();
159        let part = Part::bytes(bytes);
160
161        let output = String::from_utf8_lossy(&part.bytes);
162        assert_eq!(output, "some_text");
163    }
164
165    #[test]
166    fn it_should_use_mime_type_octet_stream() {
167        let bytes = "some_text".as_bytes();
168        let part = Part::bytes(bytes);
169
170        assert_eq!(part.mime_type, mime::APPLICATION_OCTET_STREAM);
171    }
172}
173
174#[cfg(test)]
175mod test_file_name {
176    use super::*;
177
178    #[test]
179    fn it_should_use_file_name_given() {
180        let mut part = Part::text("some_text");
181
182        assert_eq!(part.file_name, None);
183        part = part.file_name("my-text.txt");
184        assert_eq!(part.file_name, Some("my-text.txt".to_string()));
185    }
186}
187
188#[cfg(test)]
189mod test_mime_type {
190    use super::*;
191    use crate::testing::catch_panic_error_message;
192    use pretty_assertions::assert_str_eq;
193
194    #[test]
195    fn it_should_use_mime_type_set() {
196        let mut part = Part::text("some_text");
197
198        assert_eq!(part.mime_type, mime::TEXT_PLAIN);
199        part = part.mime_type("application/json");
200        assert_eq!(part.mime_type, mime::APPLICATION_JSON);
201    }
202
203    #[test]
204    fn it_should_error_if_invalid_mime_type() {
205        let part = Part::text("some_text");
206
207        let message = catch_panic_error_message(|| {
208            part.mime_type("🦊");
209        });
210        assert_str_eq!(
211            "Failed to parse '🦊' as a Mime type,
212    mime parse error: an invalid token was encountered, F0 at position 0
213",
214            message
215        );
216    }
217}