axum_test/multipart/
part.rs

1use anyhow::Context;
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            .with_context(|| format!("Failed to parse '{raw_mime_type}' as a Mime type"))
79            .unwrap();
80
81        self.mime_type = parsed_mime_type;
82
83        self
84    }
85
86    /// Adds a header to be sent with the Part of this Multiform.
87    ///
88    /// ```rust
89    /// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
90    /// #
91    /// use axum::Router;
92    /// use axum_test::TestServer;
93    /// use axum_test::multipart::MultipartForm;
94    /// use axum_test::multipart::Part;
95    ///
96    /// let app = Router::new();
97    /// let server = TestServer::new(app)?;
98    ///
99    /// let readme_bytes = include_bytes!("../../README.md");
100    /// let readme_part = Part::bytes(readme_bytes.as_slice())
101    ///     .file_name(&"README.md")
102    ///     // Add a header to the Part
103    ///     .add_header("x-text-category", "readme");
104    ///
105    /// let multipart_form = MultipartForm::new()
106    ///     .add_part("file", readme_part);
107    ///
108    /// let response = server.post(&"/my-form")
109    ///     .multipart(multipart_form)
110    ///     .await;
111    /// #
112    /// # Ok(()) }
113    /// ```
114    ///
115    pub fn add_header<N, V>(mut self, name: N, value: V) -> Self
116    where
117        N: TryInto<HeaderName>,
118        N::Error: Debug,
119        V: TryInto<HeaderValue>,
120        V::Error: Debug,
121    {
122        let header_name: HeaderName = name
123            .try_into()
124            .expect("Failed to convert header name to HeaderName");
125        let header_value: HeaderValue = value
126            .try_into()
127            .expect("Failed to convert header vlue to HeaderValue");
128
129        self.headers.push((header_name, header_value));
130        self
131    }
132}
133
134#[cfg(test)]
135mod test_text {
136    use super::*;
137
138    #[test]
139    fn it_should_contain_text_given() {
140        let part = Part::text("some_text");
141
142        let output = String::from_utf8_lossy(&part.bytes);
143        assert_eq!(output, "some_text");
144    }
145
146    #[test]
147    fn it_should_use_mime_type_text() {
148        let part = Part::text("some_text");
149        assert_eq!(part.mime_type, mime::TEXT_PLAIN);
150    }
151}
152
153#[cfg(test)]
154mod test_byes {
155    use super::*;
156
157    #[test]
158    fn it_should_contain_bytes_given() {
159        let bytes = "some_text".as_bytes();
160        let part = Part::bytes(bytes);
161
162        let output = String::from_utf8_lossy(&part.bytes);
163        assert_eq!(output, "some_text");
164    }
165
166    #[test]
167    fn it_should_use_mime_type_octet_stream() {
168        let bytes = "some_text".as_bytes();
169        let part = Part::bytes(bytes);
170
171        assert_eq!(part.mime_type, mime::APPLICATION_OCTET_STREAM);
172    }
173}
174
175#[cfg(test)]
176mod test_file_name {
177    use super::*;
178
179    #[test]
180    fn it_should_use_file_name_given() {
181        let mut part = Part::text("some_text");
182
183        assert_eq!(part.file_name, None);
184        part = part.file_name("my-text.txt");
185        assert_eq!(part.file_name, Some("my-text.txt".to_string()));
186    }
187}
188
189#[cfg(test)]
190mod test_mime_type {
191    use super::*;
192
193    #[test]
194    fn it_should_use_mime_type_set() {
195        let mut part = Part::text("some_text");
196
197        assert_eq!(part.mime_type, mime::TEXT_PLAIN);
198        part = part.mime_type("application/json");
199        assert_eq!(part.mime_type, mime::APPLICATION_JSON);
200    }
201
202    #[test]
203    #[should_panic]
204    fn it_should_error_if_invalid_mime_type() {
205        let part = Part::text("some_text");
206        part.mime_type("🦊");
207
208        assert!(false);
209    }
210}