swan_common/parsing/
client.rs1use 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 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
209pub 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}