sa_token_adapter/utils.rs
1// Author: 金书记
2//
3// 中文 | English
4// 通用工具函数模块 | Common utility functions module
5//
6// 代码流程逻辑 | Code Flow Logic
7// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8//
9// 本模块提供了框架适配器的通用工具函数,简化了新框架集成的复杂度。
10// This module provides common utility functions for framework adapters,
11// simplifying the complexity of integrating new frameworks.
12//
13// 核心功能 | Core Features:
14// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15//
16// 1. Cookie 解析
17// - parse_cookies(): 解析 HTTP Cookie 头
18// - 支持多个 cookie 对,自动处理空格
19//
20// 2. 查询字符串解析
21// - parse_query_string(): 解析 URL 查询参数
22// - 自动 URL 解码,支持特殊字符
23//
24// 3. Cookie 构建
25// - build_cookie_string(): 从 CookieOptions 构建完整的 Set-Cookie 字符串
26// - 支持所有标准 cookie 属性(Domain、Path、Max-Age、Secure、HttpOnly、SameSite)
27//
28// 4. Header 解析
29// - extract_bearer_token(): 从 Authorization 头提取 Bearer token
30//
31// 使用场景 | Use Cases:
32// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33//
34// 新框架适配器实现时,只需调用这些工具函数,无需重复编写解析逻辑:
35// When implementing a new framework adapter, just call these utility functions
36// without rewriting parsing logic:
37//
38// ```rust
39// impl SaRequest for MyFrameworkRequest {
40// fn get_cookie(&self, name: &str) -> Option<String> {
41// self.get_header("cookie")
42// .and_then(|cookies| parse_cookies(&cookies).get(name).cloned())
43// }
44// }
45// ```
46//
47// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48
49use std::collections::HashMap;
50use crate::context::CookieOptions;
51
52/// 中文 | English
53/// 解析 Cookie 字符串为键值对映射 | Parse Cookie string into key-value map
54///
55/// # 参数 | Parameters
56/// - `cookie_header`: Cookie 头字符串,格式如 "key1=value1; key2=value2"
57///
58/// # 返回 | Returns
59/// - `HashMap<String, String>`: Cookie 名称到值的映射
60///
61/// # 示例 | Example
62/// ```
63/// use sa_token_adapter::utils::parse_cookies;
64///
65/// let cookies = parse_cookies("session=abc123; user=john");
66/// assert_eq!(cookies.get("session"), Some(&"abc123".to_string()));
67/// ```
68pub fn parse_cookies(cookie_header: &str) -> HashMap<String, String> {
69 let mut cookies = HashMap::new();
70 for pair in cookie_header.split(';') {
71 let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
72 if parts.len() == 2 {
73 cookies.insert(parts[0].to_string(), parts[1].to_string());
74 }
75 }
76 cookies
77}
78
79/// 中文 | English
80/// 解析 URL 查询字符串为键值对映射 | Parse URL query string into key-value map
81///
82/// # 参数 | Parameters
83/// - `query`: 查询字符串,格式如 "key1=value1&key2=value2"
84///
85/// # 返回 | Returns
86/// - `HashMap<String, String>`: 参数名称到值的映射(自动 URL 解码)
87///
88/// # 示例 | Example
89/// ```
90/// use sa_token_adapter::utils::parse_query_string;
91///
92/// let params = parse_query_string("name=John%20Doe&age=30");
93/// assert_eq!(params.get("name"), Some(&"John Doe".to_string()));
94/// ```
95pub fn parse_query_string(query: &str) -> HashMap<String, String> {
96 let mut params = HashMap::new();
97 for pair in query.split('&') {
98 let parts: Vec<&str> = pair.splitn(2, '=').collect();
99 if parts.len() == 2 {
100 params.insert(
101 urlencoding::decode(parts[0]).unwrap_or_default().to_string(),
102 urlencoding::decode(parts[1]).unwrap_or_default().to_string(),
103 );
104 }
105 }
106 params
107}
108
109/// 中文 | English
110/// 从 CookieOptions 构建完整的 Set-Cookie 字符串 | Build complete Set-Cookie string from CookieOptions
111///
112/// # 参数 | Parameters
113/// - `name`: Cookie 名称 | Cookie name
114/// - `value`: Cookie 值 | Cookie value
115/// - `options`: Cookie 选项(Domain、Path、Max-Age 等)| Cookie options
116///
117/// # 返回 | Returns
118/// - `String`: 完整的 Set-Cookie 头值
119///
120/// # 示例 | Example
121/// ```
122/// use sa_token_adapter::{utils::build_cookie_string, CookieOptions, SameSite};
123///
124/// let cookie = build_cookie_string("session", "abc123", CookieOptions {
125/// domain: Some("example.com".to_string()),
126/// path: Some("/".to_string()),
127/// max_age: Some(3600),
128/// http_only: true,
129/// secure: true,
130/// same_site: Some(SameSite::Strict),
131/// });
132/// // 结果: "session=abc123; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Strict"
133/// ```
134pub fn build_cookie_string(name: &str, value: &str, options: CookieOptions) -> String {
135 let mut cookie = format!("{}={}", name, value);
136
137 if let Some(domain) = options.domain {
138 cookie.push_str(&format!("; Domain={}", domain));
139 }
140 if let Some(path) = options.path {
141 cookie.push_str(&format!("; Path={}", path));
142 }
143 if let Some(max_age) = options.max_age {
144 cookie.push_str(&format!("; Max-Age={}", max_age));
145 }
146 if options.http_only {
147 cookie.push_str("; HttpOnly");
148 }
149 if options.secure {
150 cookie.push_str("; Secure");
151 }
152 if let Some(same_site) = options.same_site {
153 cookie.push_str(&format!("; SameSite={}", same_site));
154 }
155
156 cookie
157}
158
159/// 中文 | English
160/// 从 Authorization 头提取 Bearer token | Extract Bearer token from Authorization header
161///
162/// # 参数 | Parameters
163/// - `auth_header`: Authorization 头值,格式如 "Bearer <token>"
164///
165/// # 返回 | Returns
166/// - `Option<String>`: 提取的 token,如果格式不正确则返回 None
167///
168/// # 示例 | Example
169/// ```
170/// use sa_token_adapter::utils::extract_bearer_token;
171///
172/// let token = extract_bearer_token("Bearer abc123xyz");
173/// assert_eq!(token, Some("abc123xyz".to_string()));
174///
175/// let invalid = extract_bearer_token("Basic xyz");
176/// assert_eq!(invalid, None);
177/// ```
178pub fn extract_bearer_token(auth_header: &str) -> Option<String> {
179 auth_header
180 .strip_prefix("Bearer ")
181 .map(|token| token.trim().to_string())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_parse_cookies() {
190 let cookies = parse_cookies("session=abc123; user=john; theme=dark");
191 assert_eq!(cookies.get("session"), Some(&"abc123".to_string()));
192 assert_eq!(cookies.get("user"), Some(&"john".to_string()));
193 assert_eq!(cookies.get("theme"), Some(&"dark".to_string()));
194 }
195
196 #[test]
197 fn test_parse_query_string() {
198 let params = parse_query_string("name=John%20Doe&age=30&city=New%20York");
199 assert_eq!(params.get("name"), Some(&"John Doe".to_string()));
200 assert_eq!(params.get("age"), Some(&"30".to_string()));
201 assert_eq!(params.get("city"), Some(&"New York".to_string()));
202 }
203
204 #[test]
205 fn test_build_cookie_string() {
206 use crate::context::SameSite;
207
208 let cookie = build_cookie_string("session", "abc123", CookieOptions {
209 domain: Some("example.com".to_string()),
210 path: Some("/".to_string()),
211 max_age: Some(3600),
212 http_only: true,
213 secure: true,
214 same_site: Some(SameSite::Strict),
215 });
216
217 assert!(cookie.contains("session=abc123"));
218 assert!(cookie.contains("Domain=example.com"));
219 assert!(cookie.contains("Path=/"));
220 assert!(cookie.contains("Max-Age=3600"));
221 assert!(cookie.contains("HttpOnly"));
222 assert!(cookie.contains("Secure"));
223 assert!(cookie.contains("SameSite=Strict"));
224 }
225
226 #[test]
227 fn test_extract_bearer_token() {
228 assert_eq!(
229 extract_bearer_token("Bearer abc123xyz"),
230 Some("abc123xyz".to_string())
231 );
232 assert_eq!(extract_bearer_token("Bearer token_with_spaces "), Some("token_with_spaces".to_string()));
233 assert_eq!(extract_bearer_token("Basic xyz"), None);
234 assert_eq!(extract_bearer_token("Bearer"), None);
235 }
236}
237