swan_common/parsing/
handler.rs

1use syn::parse::{Parse, ParseStream};
2use syn::punctuated::Punctuated;
3use syn::{LitStr, Meta, Path, Token};
4use crate::types::{ContentType, HandlerArgs, HttpMethod, RetryConfig, ProxyConfig, ProxyType};
5
6impl Parse for HandlerArgs {
7    fn parse(input: ParseStream) -> syn::Result<Self> {
8        let method = None;
9        let mut url = None;
10        let mut content_type = None;
11        let mut headers = Punctuated::new();
12        let mut interceptor = None;
13        let mut retry = None;
14        let mut proxy = None;
15
16        let pairs = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
17        for pair in pairs {
18            match pair {
19                Meta::NameValue(name_value) => {
20                    let key = name_value.path.get_ident().ok_or_else(|| {
21                        syn::Error::new_spanned(&name_value.path, "expected identifier as key")
22                    })?;
23
24                    match key.to_string().as_str() {
25                        "url" => {
26                            url = Some(parse_url_value(&name_value.value)?);
27                        }
28                        "content_type" => {
29                            content_type = Some(parse_content_type_value(&name_value.value)?);
30                        }
31                        "header" => {
32                            headers.push(parse_header_value(&name_value.value)?);
33                        }
34                        "interceptor" => {
35                            interceptor = Some(parse_interceptor_value(&name_value.value)?);
36                        }
37                        "retry" => {
38                            retry = Some(parse_retry_value(&name_value.value)?);
39                        }
40                        "proxy" => {
41                            proxy = Some(parse_proxy_simple_value(&name_value.value)?);
42                        }
43                        _ => {
44                            return Err(syn::Error::new_spanned(
45                                key,
46                                "Only 'url', 'content_type', 'header', 'interceptor', 'retry', and 'proxy' are supported",
47                            ));
48                        }
49                    }
50                }
51                Meta::List(meta_list) if meta_list.path.is_ident("proxy") => {
52                    proxy = Some(parse_proxy_full_value(&meta_list)?);
53                }
54                _ => {
55                    return Err(syn::Error::new_spanned(pair, "expected key-value pair or function-like macro"));
56                }
57            }
58        }
59
60        let url = url.ok_or_else(|| syn::Error::new(input.span(), "Missing required 'url' parameter"))?;
61        let method = method.unwrap_or(HttpMethod::Get);
62        
63        Ok(HandlerArgs {
64            method,
65            url,
66            content_type,
67            headers,
68            interceptor,
69            retry,
70            proxy,
71        })
72    }
73}
74
75fn parse_url_value(value: &syn::Expr) -> syn::Result<LitStr> {
76    if let syn::Expr::Lit(syn::ExprLit {
77        lit: syn::Lit::Str(lit),
78        ..
79    }) = value
80    {
81        Ok(lit.clone())
82    } else {
83        Err(syn::Error::new_spanned(
84            value,
85            "url must be a string literal",
86        ))
87    }
88}
89
90fn parse_content_type_value(value: &syn::Expr) -> syn::Result<ContentType> {
91    if let syn::Expr::Path(expr_path) = value {
92        let ident = expr_path.path.get_ident().ok_or_else(|| {
93            syn::Error::new_spanned(
94                &expr_path,
95                "content_type must be a simple identifier",
96            )
97        })?;
98        match ident.to_string().as_str() {
99            "json" => Ok(ContentType::Json),
100            "form_urlencoded" => Ok(ContentType::FormUrlEncoded),
101            "form_multipart" => Ok(ContentType::FormMultipart),
102            _ => {
103                Err(syn::Error::new_spanned(
104                    ident,
105                    "content_type must be one of 'json', 'form_urlencoded', or 'form_multipart'",
106                ))
107            }
108        }
109    } else {
110        Err(syn::Error::new_spanned(
111            value,
112            "content_type must be an identifier (e.g., json, form_urlencoded, or form_multipart)",
113        ))
114    }
115}
116
117fn parse_header_value(value: &syn::Expr) -> syn::Result<LitStr> {
118    if let syn::Expr::Lit(syn::ExprLit {
119        lit: syn::Lit::Str(lit),
120        ..
121    }) = value
122    {
123        Ok(lit.clone())
124    } else {
125        Err(syn::Error::new_spanned(
126            value,
127            "header must be a string literal",
128        ))
129    }
130}
131
132fn parse_interceptor_value(value: &syn::Expr) -> syn::Result<Path> {
133    if let syn::Expr::Path(expr_path) = value {
134        Ok(expr_path.path.clone())
135    } else {
136        Err(syn::Error::new_spanned(
137            value,
138            "interceptor must be a trait path",
139        ))
140    }
141}
142
143fn parse_retry_value(value: &syn::Expr) -> syn::Result<RetryConfig> {
144    if let syn::Expr::Lit(syn::ExprLit {
145        lit: syn::Lit::Str(lit),
146        ..
147    }) = value
148    {
149        RetryConfig::parse(lit)
150    } else {
151        Err(syn::Error::new_spanned(
152            value,
153            "retry must be a string literal (e.g., \"exponential(3, 100ms)\")",
154        ))
155    }
156}
157
158fn parse_proxy_simple_value(value: &syn::Expr) -> syn::Result<ProxyConfig> {
159    match value {
160        syn::Expr::Lit(syn::ExprLit {
161            lit: syn::Lit::Str(lit),
162            ..
163        }) => Ok(ProxyConfig::Simple(lit.clone())),
164        syn::Expr::Lit(syn::ExprLit {
165            lit: syn::Lit::Bool(lit),
166            ..
167        }) => {
168            if lit.value {
169                Err(syn::Error::new_spanned(
170                    value,
171                    "proxy = true is not supported, use proxy = \"url\" instead",
172                ))
173            } else {
174                Ok(ProxyConfig::Disabled(lit.clone()))
175            }
176        }
177        _ => Err(syn::Error::new_spanned(
178            value,
179            "proxy must be a string literal (URL) or false (to disable)",
180        ))
181    }
182}
183
184fn parse_proxy_full_value(meta_list: &syn::MetaList) -> syn::Result<ProxyConfig> {
185    let mut proxy_type = None;
186    let mut url = None;
187    let mut username = None;
188    let mut password = None;
189    let mut no_proxy = None;
190
191    let nested = meta_list.parse_args_with(Punctuated::<syn::Meta, Token![,]>::parse_terminated)?;
192    
193    for meta in nested {
194        if let syn::Meta::NameValue(nv) = meta {
195            if nv.path.is_ident("type") {
196                if let syn::Expr::Path(expr_path) = &nv.value {
197                    if let Some(ident) = expr_path.path.get_ident() {
198                        let type_str = ident.to_string();
199                        proxy_type = ProxyType::from_str(&type_str);
200                        if proxy_type.is_none() {
201                            return Err(syn::Error::new_spanned(
202                                &nv.value, 
203                                "proxy type must be 'http' or 'socks5'"
204                            ));
205                        }
206                    } else {
207                        return Err(syn::Error::new_spanned(&nv.value, "proxy type must be an identifier"));
208                    }
209                } else {
210                    return Err(syn::Error::new_spanned(&nv.value, "proxy type must be an identifier"));
211                }
212            } else if nv.path.is_ident("url") {
213                if let syn::Expr::Lit(syn::ExprLit {
214                    lit: syn::Lit::Str(lit),
215                    ..
216                }) = &nv.value {
217                    url = Some(lit.clone());
218                } else {
219                    return Err(syn::Error::new_spanned(&nv.value, "url must be a string literal"));
220                }
221            } else if nv.path.is_ident("username") {
222                if let syn::Expr::Lit(syn::ExprLit {
223                    lit: syn::Lit::Str(lit),
224                    ..
225                }) = &nv.value {
226                    username = Some(lit.clone());
227                } else {
228                    return Err(syn::Error::new_spanned(&nv.value, "username must be a string literal"));
229                }
230            } else if nv.path.is_ident("password") {
231                if let syn::Expr::Lit(syn::ExprLit {
232                    lit: syn::Lit::Str(lit),
233                    ..
234                }) = &nv.value {
235                    password = Some(lit.clone());
236                } else {
237                    return Err(syn::Error::new_spanned(&nv.value, "password must be a string literal"));
238                }
239            } else if nv.path.is_ident("no_proxy") {
240                if let syn::Expr::Lit(syn::ExprLit {
241                    lit: syn::Lit::Str(lit),
242                    ..
243                }) = &nv.value {
244                    no_proxy = Some(lit.clone());
245                } else {
246                    return Err(syn::Error::new_spanned(&nv.value, "no_proxy must be a string literal"));
247                }
248            } else {
249                return Err(syn::Error::new_spanned(
250                    &nv.path,
251                    "Only 'type', 'url', 'username', 'password', or 'no_proxy' are supported in proxy configuration",
252                ));
253            }
254        } else {
255            return Err(syn::Error::new_spanned(meta, "Expected key-value pair in proxy configuration"));
256        }
257    }
258
259    let url = url.ok_or_else(|| {
260        syn::Error::new_spanned(&meta_list.path, "proxy configuration must include 'url'")
261    })?;
262
263    Ok(ProxyConfig::Full {
264        proxy_type,
265        url,
266        username,
267        password,
268        no_proxy,
269    })
270}
271
272/// 解析处理器参数的公共函数
273pub fn parse_handler_args(input: ParseStream) -> syn::Result<HandlerArgs> {
274    HandlerArgs::parse(input)
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use syn::parse_quote;
281
282    #[test]
283    fn test_parse_url_value() {
284        let expr = parse_quote! { "/api/test" };
285        let result = parse_url_value(&expr).unwrap();
286        assert_eq!(result.value(), "/api/test");
287    }
288
289    #[test]
290    fn test_parse_content_type_value() {
291        let expr = parse_quote! { json };
292        let result = parse_content_type_value(&expr).unwrap();
293        assert_eq!(result, ContentType::Json);
294    }
295
296    #[test]
297    fn test_parse_header_value() {
298        let expr = parse_quote! { "Authorization: Bearer token" };
299        let result = parse_header_value(&expr).unwrap();
300        assert_eq!(result.value(), "Authorization: Bearer token");
301    }
302}