ferrin_message/
file_source.rs1use std::path::PathBuf;
4
5use base64::Engine;
6use base64::prelude::BASE64_STANDARD;
7use bytes::Bytes;
8use ferrin_spec::FileData;
9use ferrin_spec::ProviderReference;
10use serde::Deserialize;
11use serde::Serialize;
12use url::Url;
13
14use crate::data_url;
15use crate::data_url::DataUrl;
16use crate::error::FileSourceError;
17use crate::error::InvalidDataContentError;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "type", rename_all = "kebab-case")]
27#[non_exhaustive]
28pub enum FileSource {
29 #[serde(rename = "data")]
31 Bytes {
32 #[serde(with = "base64_codec")]
34 data: Bytes,
35 },
36 Base64 {
38 data: String,
40 },
41 Url {
43 url: Url,
45 },
46 Reference {
48 reference: ProviderReference,
50 },
51 Text {
53 text: String,
55 },
56 Path {
58 path: PathBuf,
60 },
61}
62
63impl FileSource {
64 #[must_use]
66 pub fn bytes(data: impl Into<Bytes>) -> Self {
67 Self::Bytes { data: data.into() }
68 }
69
70 #[must_use]
72 pub fn base64(data: impl Into<String>) -> Self {
73 Self::Base64 { data: data.into() }
74 }
75
76 #[must_use]
78 pub fn url(url: Url) -> Self {
79 Self::Url { url }
80 }
81
82 #[must_use]
84 pub fn reference(provider: impl Into<String>, id: impl Into<String>) -> Self {
85 let mut map = ProviderReference::new();
86 map.insert(provider.into(), id.into());
87 Self::Reference { reference: map }
88 }
89
90 #[must_use]
92 pub fn text(text: impl Into<String>) -> Self {
93 Self::Text { text: text.into() }
94 }
95
96 #[must_use]
98 pub fn path(path: impl Into<PathBuf>) -> Self {
99 Self::Path { path: path.into() }
100 }
101
102 pub fn parse_url(url: &str) -> Result<Self, url::ParseError> {
108 Url::parse(url).map(|url| Self::Url { url })
109 }
110
111 #[must_use]
113 pub fn as_bytes(&self) -> Option<&Bytes> {
114 match self {
115 Self::Bytes { data } => Some(data),
116 _ => None,
117 }
118 }
119
120 #[must_use]
122 pub fn as_url(&self) -> Option<&Url> {
123 match self {
124 Self::Url { url } => Some(url),
125 _ => None,
126 }
127 }
128
129 #[must_use]
131 pub fn as_reference(&self) -> Option<&ProviderReference> {
132 match self {
133 Self::Reference { reference } => Some(reference),
134 _ => None,
135 }
136 }
137
138 #[must_use]
140 pub fn is_data_url(&self) -> bool {
141 matches!(self, Self::Url { url } if url.scheme().eq_ignore_ascii_case("data"))
142 }
143
144 pub fn data_url(&self) -> Option<Result<DataUrl, InvalidDataContentError>> {
150 match self {
151 Self::Url { url } if url.scheme().eq_ignore_ascii_case("data") => {
152 Some(data_url::parse(url.as_str()))
153 }
154 _ => None,
155 }
156 }
157
158 pub fn decoded_bytes(&self) -> Result<Option<Bytes>, InvalidDataContentError> {
166 match self {
167 Self::Bytes { data } => Ok(Some(data.clone())),
168 Self::Base64 { data } => decode_base64(data).map(Some),
169 _ => Ok(None),
170 }
171 }
172}
173
174impl From<FileData> for FileSource {
175 fn from(data: FileData) -> Self {
176 match data {
177 FileData::Bytes { data } => Self::Bytes { data },
178 FileData::Url { url } => Self::Url { url },
179 FileData::Reference { reference } => Self::Reference { reference },
180 FileData::Text { text } => Self::Text { text },
181 _ => unreachable!("every FileData variant has a FileSource counterpart"),
184 }
185 }
186}
187
188impl TryFrom<FileSource> for FileData {
189 type Error = FileSourceError;
190
191 fn try_from(source: FileSource) -> Result<Self, Self::Error> {
195 match source {
196 FileSource::Bytes { data } => Ok(Self::Bytes { data }),
197 FileSource::Base64 { data } => Ok(Self::Bytes {
198 data: decode_base64(&data)?,
199 }),
200 FileSource::Url { url } => Ok(Self::Url { url }),
201 FileSource::Reference { reference } => Ok(Self::Reference { reference }),
202 FileSource::Text { text } => Ok(Self::Text { text }),
203 FileSource::Path { path } => Err(FileSourceError::UnreadPath { path }),
204 }
205 }
206}
207
208impl From<Bytes> for FileSource {
209 fn from(data: Bytes) -> Self {
210 Self::Bytes { data }
211 }
212}
213
214impl From<Vec<u8>> for FileSource {
215 fn from(bytes: Vec<u8>) -> Self {
216 Self::Bytes {
217 data: Bytes::from(bytes),
218 }
219 }
220}
221
222impl From<Url> for FileSource {
223 fn from(url: Url) -> Self {
224 Self::Url { url }
225 }
226}
227
228impl From<ProviderReference> for FileSource {
229 fn from(reference: ProviderReference) -> Self {
230 Self::Reference { reference }
231 }
232}
233
234impl From<PathBuf> for FileSource {
235 fn from(path: PathBuf) -> Self {
236 Self::Path { path }
237 }
238}
239
240fn decode_base64(text: &str) -> Result<Bytes, InvalidDataContentError> {
242 use base64::alphabet::STANDARD;
243 use base64::engine::DecodePaddingMode;
244 use base64::engine::GeneralPurpose;
245 use base64::engine::GeneralPurposeConfig;
246
247 const LENIENT: GeneralPurpose = GeneralPurpose::new(
248 &STANDARD,
249 GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
250 );
251 let compact: Vec<u8> = text
252 .bytes()
253 .filter(|byte| !byte.is_ascii_whitespace())
254 .collect();
255 LENIENT.decode(compact).map(Bytes::from).map_err(|error| {
256 InvalidDataContentError::new("content is not valid base64").with_cause(error)
257 })
258}
259
260mod base64_codec {
261 use super::BASE64_STANDARD;
262 use super::Engine;
263 use bytes::Bytes;
264 use serde::Deserialize;
265 use serde::Deserializer;
266 use serde::Serializer;
267
268 pub(super) fn serialize<S: Serializer>(
269 bytes: &Bytes,
270 serializer: S,
271 ) -> Result<S::Ok, S::Error> {
272 serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
273 }
274
275 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
276 deserializer: D,
277 ) -> Result<Bytes, D::Error> {
278 let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
279 super::decode_base64(&text).map_err(serde::de::Error::custom)
280 }
281}