swan_common/parsing/
client.rs

1use syn::parse::{Parse, ParseStream};
2use syn::punctuated::Punctuated;
3use syn::{LitStr, Path, Token};
4use crate::types::{HttpClientArgs, ProxyConfig, ProxyType};
5
6impl Parse for HttpClientArgs {
7    fn parse(input: ParseStream) -> syn::Result<Self> {
8        let mut base_url = None;
9        let mut interceptor = None;
10        let mut state = None;
11        let mut proxy = None;
12
13        let pairs = Punctuated::<syn::Meta, Token![,]>::parse_terminated(input)?;
14        for meta in pairs {
15            match meta {
16                syn::Meta::NameValue(nv) => {
17                    if nv.path.is_ident("base_url") {
18                        base_url = Some(parse_base_url_value(&nv.value)?);
19                    } else if nv.path.is_ident("interceptor") {
20                        interceptor = Some(parse_interceptor_value(&nv.value)?);
21                    } else if nv.path.is_ident("state") {
22                        state = Some(parse_state_value(&nv.value)?);
23                    } else if nv.path.is_ident("proxy") {
24                        proxy = Some(parse_proxy_simple_value(&nv.value)?);
25                    } else {
26                        return Err(syn::Error::new_spanned(
27                            nv.path,
28                            "Only 'base_url', 'interceptor', 'state', or 'proxy' are supported",
29                        ));
30                    }
31                }
32                syn::Meta::List(ml) if ml.path.is_ident("proxy") => {
33                    proxy = Some(parse_proxy_full_value(&ml)?);
34                }
35                _ => {
36                    return Err(syn::Error::new_spanned(meta, "Expected key-value pair or function-like macro"));
37                }
38            }
39        }
40
41        // 验证:如果使用了 state,必须同时提供 interceptor
42        if state.is_some() && interceptor.is_none() {
43            return Err(syn::Error::new(
44                input.span(),
45                "When using 'state', 'interceptor' must also be provided"
46            ));
47        }
48
49        Ok(HttpClientArgs {
50            base_url,
51            interceptor,
52            state,
53            proxy,
54        })
55    }
56}
57
58fn parse_base_url_value(value: &syn::Expr) -> syn::Result<LitStr> {
59    if let syn::Expr::Lit(syn::ExprLit {
60        lit: syn::Lit::Str(lit),
61        ..
62    }) = value
63    {
64        Ok(lit.clone())
65    } else {
66        Err(syn::Error::new_spanned(
67            value,
68            "base_url must be a string literal",
69        ))
70    }
71}
72
73fn parse_interceptor_value(value: &syn::Expr) -> syn::Result<Path> {
74    if let syn::Expr::Path(expr_path) = value {
75        Ok(expr_path.path.clone())
76    } else {
77        Err(syn::Error::new_spanned(
78            value,
79            "interceptor must be a trait path",
80        ))
81    }
82}
83
84fn parse_state_value(value: &syn::Expr) -> syn::Result<Path> {
85    if let syn::Expr::Path(expr_path) = value {
86        Ok(expr_path.path.clone())
87    } else {
88        Err(syn::Error::new_spanned(
89            value,
90            "state must be a type path",
91        ))
92    }
93}
94
95fn parse_proxy_simple_value(value: &syn::Expr) -> syn::Result<ProxyConfig> {
96    match value {
97        syn::Expr::Lit(syn::ExprLit {
98            lit: syn::Lit::Str(lit),
99            ..
100        }) => Ok(ProxyConfig::Simple(lit.clone())),
101        syn::Expr::Lit(syn::ExprLit {
102            lit: syn::Lit::Bool(lit),
103            ..
104        }) => {
105            if lit.value {
106                Err(syn::Error::new_spanned(
107                    value,
108                    "proxy = true is not supported, use proxy = \"url\" instead",
109                ))
110            } else {
111                Ok(ProxyConfig::Disabled(lit.clone()))
112            }
113        }
114        _ => Err(syn::Error::new_spanned(
115            value,
116            "proxy must be a string literal (URL) or false (to disable)",
117        ))
118    }
119}
120
121fn parse_proxy_full_value(meta_list: &syn::MetaList) -> syn::Result<ProxyConfig> {
122    let mut proxy_type = None;
123    let mut url = None;
124    let mut username = None;
125    let mut password = None;
126    let mut no_proxy = None;
127
128    let nested = meta_list.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated)?;
129    
130    for meta in nested {
131        if let syn::Meta::NameValue(nv) = meta {
132            if nv.path.is_ident("type") {
133                if let syn::Expr::Path(expr_path) = &nv.value {
134                    if let Some(ident) = expr_path.path.get_ident() {
135                        let type_str = ident.to_string();
136                        proxy_type = ProxyType::from_str(&type_str);
137                        if proxy_type.is_none() {
138                            return Err(syn::Error::new_spanned(
139                                &nv.value, 
140                                "proxy type must be 'http' or 'socks5'"
141                            ));
142                        }
143                    } else {
144                        return Err(syn::Error::new_spanned(&nv.value, "proxy type must be an identifier"));
145                    }
146                } else {
147                    return Err(syn::Error::new_spanned(&nv.value, "proxy type must be an identifier"));
148                }
149            } else if nv.path.is_ident("url") {
150                if let syn::Expr::Lit(syn::ExprLit {
151                    lit: syn::Lit::Str(lit),
152                    ..
153                }) = &nv.value {
154                    url = Some(lit.clone());
155                } else {
156                    return Err(syn::Error::new_spanned(&nv.value, "url must be a string literal"));
157                }
158            } else if nv.path.is_ident("username") {
159                if let syn::Expr::Lit(syn::ExprLit {
160                    lit: syn::Lit::Str(lit),
161                    ..
162                }) = &nv.value {
163                    username = Some(lit.clone());
164                } else {
165                    return Err(syn::Error::new_spanned(&nv.value, "username must be a string literal"));
166                }
167            } else if nv.path.is_ident("password") {
168                if let syn::Expr::Lit(syn::ExprLit {
169                    lit: syn::Lit::Str(lit),
170                    ..
171                }) = &nv.value {
172                    password = Some(lit.clone());
173                } else {
174                    return Err(syn::Error::new_spanned(&nv.value, "password must be a string literal"));
175                }
176            } else if nv.path.is_ident("no_proxy") {
177                if let syn::Expr::Lit(syn::ExprLit {
178                    lit: syn::Lit::Str(lit),
179                    ..
180                }) = &nv.value {
181                    no_proxy = Some(lit.clone());
182                } else {
183                    return Err(syn::Error::new_spanned(&nv.value, "no_proxy must be a string literal"));
184                }
185            } else {
186                return Err(syn::Error::new_spanned(
187                    &nv.path,
188                    "Only 'type', 'url', 'username', 'password', or 'no_proxy' are supported in proxy configuration",
189                ));
190            }
191        } else {
192            return Err(syn::Error::new_spanned(meta, "Expected key-value pair in proxy configuration"));
193        }
194    }
195
196    let url = url.ok_or_else(|| {
197        syn::Error::new_spanned(&meta_list.path, "proxy configuration must include 'url'")
198    })?;
199
200    Ok(ProxyConfig::Full {
201        proxy_type,
202        url,
203        username,
204        password,
205        no_proxy,
206    })
207}
208
209/// 解析HTTP客户端参数的公共函数
210pub fn parse_http_client_args(input: ParseStream) -> syn::Result<HttpClientArgs> {
211    HttpClientArgs::parse(input)
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use syn::parse_quote;
218    use quote::quote;
219
220    #[test]
221    fn test_parse_base_url_value() {
222        let expr = parse_quote! { "https://api.example.com" };
223        let result = parse_base_url_value(&expr).unwrap();
224        assert_eq!(result.value(), "https://api.example.com");
225    }
226
227    #[test]
228    fn test_parse_interceptor_value() {
229        let expr = parse_quote! { MyInterceptor };
230        let result = parse_interceptor_value(&expr).unwrap();
231        assert_eq!(result.segments.len(), 1);
232        assert_eq!(result.segments.first().unwrap().ident.to_string(), "MyInterceptor");
233    }
234
235    #[test]
236    fn test_invalid_base_url() {
237        let expr = parse_quote! { 123 };
238        let result = parse_base_url_value(&expr);
239        assert!(result.is_err());
240    }
241
242    #[test]
243    fn test_state_without_interceptor_should_fail() {
244        let tokens = quote! { state = MyState };
245        let result = syn::parse2::<HttpClientArgs>(tokens);
246        assert!(result.is_err());
247        if let Err(err) = result {
248            assert!(err.to_string().contains("When using 'state', 'interceptor' must also be provided"));
249        }
250    }
251
252    #[test]
253    fn test_state_with_interceptor_should_succeed() {
254        let tokens = quote! { interceptor = MyInterceptor, state = MyState };
255        let result = syn::parse2::<HttpClientArgs>(tokens);
256        assert!(result.is_ok());
257    }
258}