prima_bridge/request/
body.rs

1use std::borrow::Cow;
2
3use reqwest::multipart::Part;
4use serde::Serialize;
5
6use crate::prelude::{PrimaBridgeError, PrimaBridgeResult};
7
8#[derive(Debug)]
9/// A request body.
10///
11/// It can be an in-memory buffer, a file, or a stream.
12///
13/// See the various `From` implementations for ways to construct a `Body`.
14pub struct Body {
15    pub(crate) inner: reqwest::Body,
16}
17
18impl Body {
19    /// Construct a `Body` from a streamable type.
20    ///
21    /// Generally, you can just use the various `From` implementations to create a `Body` instead.
22    ///
23    /// This is provided for cases where you don't want to load the entire body into memory at once,
24    /// and instead want to stream it from something like a file handle.
25    pub fn from_stream<S>(stream: S) -> Body
26    where
27        S: futures::stream::TryStream + Send + Sync + 'static,
28        S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
29        bytes::Bytes: From<S::Ok>,
30    {
31        Self {
32            inner: reqwest::Body::wrap_stream(stream),
33        }
34    }
35
36    /// Returns the raw bytes of the body, if it is in memory.
37    ///
38    /// This will return `None` for a body that is streaming from a file or other source, for example.
39    pub fn as_bytes(&self) -> Option<&[u8]> {
40        self.inner.as_bytes()
41    }
42
43    #[cfg(test)]
44    pub(crate) fn as_str(&self) -> Option<Cow<'_, str>> {
45        self.inner.as_bytes().map(String::from_utf8_lossy)
46    }
47}
48
49impl From<String> for Body {
50    fn from(val: String) -> Self {
51        Self {
52            inner: val.into_bytes().into(),
53        }
54    }
55}
56
57impl From<&'static str> for Body {
58    fn from(val: &'static str) -> Self {
59        Self {
60            inner: val.to_string().into_bytes().into(),
61        }
62    }
63}
64
65impl From<&'static [u8]> for Body {
66    fn from(val: &'static [u8]) -> Self {
67        Self {
68            inner: val.to_vec().into(),
69        }
70    }
71}
72
73impl From<Vec<u8>> for Body {
74    fn from(value: Vec<u8>) -> Self {
75        Self { inner: value.into() }
76    }
77}
78
79impl From<tokio::fs::File> for Body {
80    fn from(file: tokio::fs::File) -> Self {
81        Self { inner: file.into() }
82    }
83}
84
85#[allow(clippy::upper_case_acronyms)]
86#[derive(Debug, Serialize)]
87#[cfg_attr(test, derive(serde::Deserialize))]
88pub struct GraphQLBody<T> {
89    pub(crate) query: String,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub(crate) variables: Option<T>,
92}
93
94impl<T: Serialize> From<(&str, Option<T>)> for GraphQLBody<T> {
95    fn from((query, variables): (&str, Option<T>)) -> Self {
96        Self {
97            query: query.to_owned(),
98            variables,
99        }
100    }
101}
102
103impl<T: Serialize> From<(String, Option<T>)> for GraphQLBody<T> {
104    fn from((query, variables): (String, Option<T>)) -> Self {
105        (query.as_str(), variables).into()
106    }
107}
108
109impl<T: Serialize> From<(String, T)> for GraphQLBody<T> {
110    fn from((query, variables): (String, T)) -> Self {
111        (query.as_str(), Some(variables)).into()
112    }
113}
114
115#[derive(Debug)]
116/// A multipart-form file, containing the file's desired name, its MIME type, and its contents as an in-memory buffer or stream.
117pub struct MultipartFile {
118    pub(crate) content: Body,
119    pub(crate) name_opt: Option<String>,
120    pub(crate) mime_type_opt: Option<String>,
121}
122
123impl MultipartFile {
124    pub fn new(content: impl Into<Body>) -> Self {
125        Self {
126            content: content.into(),
127            name_opt: None,
128            mime_type_opt: None,
129        }
130    }
131
132    pub fn with_name(self, name: impl Into<String>) -> Self {
133        Self {
134            name_opt: Some(name.into()),
135            ..self
136        }
137    }
138
139    pub fn with_mime_type(self, mime_type: impl Into<String>) -> Self {
140        Self {
141            mime_type_opt: Some(mime_type.into()),
142            ..self
143        }
144    }
145
146    pub(crate) fn into_part(self) -> PrimaBridgeResult<Part> {
147        let mut part = Part::stream(self.content.inner);
148        if let Some(name) = self.name_opt {
149            part = part.file_name(name);
150        }
151        if let Some(mime) = self.mime_type_opt {
152            part = part
153                .mime_str(mime.as_str())
154                .map_err(|_| PrimaBridgeError::InvalidMultipartFileMimeType(mime.to_string()))?;
155        }
156        Ok(part)
157    }
158}
159
160#[derive(Debug)]
161/// A named multipart-form field which contains a file.
162pub struct MultipartFormFileField {
163    pub(crate) field_name: Cow<'static, str>,
164    pub(crate) file: MultipartFile,
165}
166impl MultipartFormFileField {
167    pub fn new<S>(file_name: S, file: MultipartFile) -> Self
168    where
169        S: Into<Cow<'static, str>>,
170    {
171        Self {
172            field_name: file_name.into(),
173            file,
174        }
175    }
176}
177impl std::hash::Hash for MultipartFormFileField {
178    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
179        self.field_name.hash(state);
180    }
181}
182impl Eq for MultipartFormFileField {}
183impl PartialEq for MultipartFormFileField {
184    fn eq(&self, other: &Self) -> bool {
185        self.field_name == other.field_name
186    }
187}