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
272pub 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}