get_data_url/
lib.rs

1use std::fmt::Display;
2
3use base64_url::encode;
4use mime::Mime;
5use percent_encoding::{NON_ALPHANUMERIC, percent_encode};
6pub use reqwest::Error;
7use reqwest::{Client, header::CONTENT_TYPE};
8
9/// Data URL 结构体,表示一个符合 RFC 2397 标准的数据 URL
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DataUrl {
12    /// 媒体类型 (MIME type)
13    pub media_type: String,
14    /// 是否是 base64 编码
15    pub base64_encoded: bool,
16    /// 数据内容
17    pub data: Vec<u8>,
18}
19
20impl DataUrl {
21    /// 创建一个新的 DataUrl
22    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
31/// 将 DataUrl 转换为字符串表示形式
32impl 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            encode(&self.data)
37        } else {
38            // 对于非 base64 编码,需要确保数据是 URL 安全的
39            percent_encode(&self.data, NON_ALPHANUMERIC).to_string()
40        };
41        write!(f, "data:{}{},{}", self.media_type, encoding, data)
42    }
43}
44
45/// HTTP 到 Data URL 转换器
46#[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    /// 创建一个新的转换器实例
59    pub fn new() -> Self {
60        Self {
61            client: Client::new(),
62        }
63    }
64
65    /// 使用自定义 HTTP 客户端创建转换器实例
66    pub fn with_client(client: Client) -> Self {
67        Self { client }
68    }
69
70    /// 从 URL 获取资源并转换为 DataUrl
71    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    /// 将 HTTP 响应转换为 DataUrl
78    pub async fn response_to_data_url(
79        &self,
80        response: reqwest::Response,
81    ) -> Result<DataUrl, Error> {
82        // 获取内容类型
83        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        // 读取响应字节
92        let bytes = response.bytes().await?.to_vec();
93
94        // 创建 DataUrl (总是使用 base64 编码以确保数据安全)
95        Ok(DataUrl::new(content_type, bytes, true))
96    }
97}
98
99/// 便捷函数:从 URL 获取资源并转换为 Data URL 字符串
100pub 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        // 设置模拟响应
129        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}