1use std::fmt::Display;
2
3use base64::{Engine as _, engine::general_purpose::STANDARD};
4use mime::Mime;
5use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
6pub use reqwest::Error;
7use reqwest::{Client, header::CONTENT_TYPE};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DataUrl {
12 pub media_type: String,
14 pub base64_encoded: bool,
16 pub data: Vec<u8>,
18}
19
20impl DataUrl {
21 pub fn new(media_type: impl Into<String>, data: Vec<u8>, base64_encoded: bool) -> Self {
23 Self {
24 media_type: media_type.into(),
25 base64_encoded,
26 data,
27 }
28 }
29}
30
31impl Display for DataUrl {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 let encoding = if self.base64_encoded { ";base64" } else { "" };
35 let data = if self.base64_encoded {
36 STANDARD.encode(&self.data)
37 } else {
38 percent_encode(&self.data, NON_ALPHANUMERIC).to_string()
40 };
41 write!(f, "data:{}{},{}", self.media_type, encoding, data)
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct GetDataUrl {
48 client: Client,
49}
50
51impl Default for GetDataUrl {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl GetDataUrl {
58 pub fn new() -> Self {
60 Self {
61 client: Client::new(),
62 }
63 }
64
65 pub fn with_client(client: Client) -> Self {
67 Self { client }
68 }
69
70 pub async fn fetch(&self, url: &str) -> Result<DataUrl, reqwest::Error> {
72 let response = self.client.get(url).send().await?;
73 println!("{:?}", response);
74 self.response_to_data_url(response).await
75 }
76
77 pub async fn response_to_data_url(
79 &self,
80 response: reqwest::Response,
81 ) -> Result<DataUrl, Error> {
82 let content_type = response
84 .headers()
85 .get(CONTENT_TYPE)
86 .and_then(|value| value.to_str().ok())
87 .and_then(|value| value.parse::<Mime>().ok())
88 .map(|mime| mime.to_string())
89 .unwrap_or_else(|| "application/octet-stream".to_string());
90
91 let bytes = response.bytes().await?.to_vec();
93
94 Ok(DataUrl::new(content_type, bytes, true))
96 }
97}
98
99pub async fn url_to_data_url(url: &str) -> Result<String, Error> {
101 let converter = GetDataUrl::new();
102 let data_url = converter.fetch(url).await?;
103 Ok(data_url.to_string())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use wiremock::matchers::method;
110 use wiremock::{Mock, MockServer, ResponseTemplate};
111
112 #[tokio::test]
113 async fn test_data_url_creation() {
114 let data = DataUrl::new("text/plain".to_string(), b"Hello, World!".to_vec(), true);
115
116 assert_eq!(data.media_type, "text/plain");
117 assert!(data.base64_encoded);
118 assert_eq!(data.data, b"Hello, World!");
119
120 let expected_string = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==";
121 assert_eq!(data.to_string(), expected_string);
122 }
123
124 #[tokio::test]
125 async fn test_fetch_data_url() {
126 let mock_server = MockServer::start().await;
127
128 Mock::given(method("GET"))
130 .respond_with(ResponseTemplate::new(200).set_body_string("Hello, World!"))
131 .mount(&mock_server)
132 .await;
133
134 let converter = GetDataUrl::new();
135 let result = converter.fetch(&mock_server.uri()).await;
136
137 assert!(result.is_ok());
138
139 let data_url = result.unwrap();
140 assert_eq!(data_url.media_type, "text/plain");
141 assert!(data_url.base64_encoded);
142 assert_eq!(data_url.data, b"Hello, World!");
143 }
144
145 #[tokio::test]
146 async fn test_url_to_data_url_convenience() {
147 let mock_server = MockServer::start().await;
148
149 Mock::given(method("GET"))
150 .respond_with(
151 ResponseTemplate::new(200).set_body_json(r#"{"message": "Hello, World!"}"#),
152 )
153 .mount(&mock_server)
154 .await;
155
156 let result = url_to_data_url(&mock_server.uri()).await;
157 assert!(result.is_ok());
158
159 let data_url_str = result.unwrap();
160 println!("{}", data_url_str);
161 assert!(data_url_str.starts_with("data:application/json;base64,"));
162 }
163
164 #[tokio::test]
165 async fn test_invalid_url() {
166 let converter = GetDataUrl::new();
167 let result = converter.fetch("not_a_valid_url").await;
168 assert!(result.is_err());
169 }
170}