A better Rust ATProto crate
1//! Attribute parsing functions
2
3use super::types::*;
4use syn::{Attribute, DeriveInput, LitStr};
5
6/// Parse type-level lexicon attributes
7pub fn parse_type_attrs(attrs: &[Attribute]) -> syn::Result<LexiconTypeAttrs> {
8 let mut result = LexiconTypeAttrs::default();
9
10 for attr in attrs {
11 if !attr.path().is_ident("lexicon") {
12 continue;
13 }
14
15 attr.parse_nested_meta(|meta| {
16 if meta.path.is_ident("nsid") {
17 let value = meta.value()?;
18 let lit: LitStr = value.parse()?;
19 result.nsid = Some(lit.value());
20 Ok(())
21 } else if meta.path.is_ident("fragment") {
22 if meta.input.peek(syn::Token![=]) {
23 let value = meta.value()?;
24 let lit: LitStr = value.parse()?;
25 result.fragment = Some(lit.value());
26 } else {
27 result.fragment = Some(String::new());
28 }
29 Ok(())
30 } else if meta.path.is_ident("record") {
31 result.kind = Some(LexiconTypeKind::Record);
32 Ok(())
33 } else if meta.path.is_ident("query") {
34 result.kind = Some(LexiconTypeKind::Query);
35 Ok(())
36 } else if meta.path.is_ident("procedure") {
37 result.kind = Some(LexiconTypeKind::Procedure);
38 Ok(())
39 } else if meta.path.is_ident("subscription") {
40 result.kind = Some(LexiconTypeKind::Subscription);
41 Ok(())
42 } else if meta.path.is_ident("key") {
43 let value = meta.value()?;
44 let lit: LitStr = value.parse()?;
45 result.key = Some(lit.value());
46 Ok(())
47 } else {
48 Err(meta.error("unknown lexicon attribute"))
49 }
50 })?;
51 }
52
53 Ok(result)
54}
55
56/// Parse field-level lexicon attributes
57pub fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<LexiconFieldAttrs> {
58 let mut result = LexiconFieldAttrs::default();
59
60 for attr in attrs {
61 if !attr.path().is_ident("lexicon") {
62 continue;
63 }
64
65 attr.parse_nested_meta(|meta| {
66 if meta.path.is_ident("max_length") {
67 let value = meta.value()?;
68 let lit: syn::LitInt = value.parse()?;
69 result.max_length = Some(lit.base10_parse()?);
70 Ok(())
71 } else if meta.path.is_ident("max_graphemes") {
72 let value = meta.value()?;
73 let lit: syn::LitInt = value.parse()?;
74 result.max_graphemes = Some(lit.base10_parse()?);
75 Ok(())
76 } else if meta.path.is_ident("min_length") {
77 let value = meta.value()?;
78 let lit: syn::LitInt = value.parse()?;
79 result.min_length = Some(lit.base10_parse()?);
80 Ok(())
81 } else if meta.path.is_ident("min_graphemes") {
82 let value = meta.value()?;
83 let lit: syn::LitInt = value.parse()?;
84 result.min_graphemes = Some(lit.base10_parse()?);
85 Ok(())
86 } else if meta.path.is_ident("minimum") {
87 let value = meta.value()?;
88 let lit: syn::LitInt = value.parse()?;
89 result.minimum = Some(lit.base10_parse()?);
90 Ok(())
91 } else if meta.path.is_ident("maximum") {
92 let value = meta.value()?;
93 let lit: syn::LitInt = value.parse()?;
94 result.maximum = Some(lit.base10_parse()?);
95 Ok(())
96 } else if meta.path.is_ident("max_items") {
97 let value = meta.value()?;
98 let lit: syn::LitInt = value.parse()?;
99 result.max_items = Some(lit.base10_parse()?);
100 Ok(())
101 } else if meta.path.is_ident("min_items") {
102 let value = meta.value()?;
103 let lit: syn::LitInt = value.parse()?;
104 result.min_items = Some(lit.base10_parse()?);
105 Ok(())
106 } else if meta.path.is_ident("item_max_length") {
107 let value = meta.value()?;
108 let lit: syn::LitInt = value.parse()?;
109 result.item_max_length = Some(lit.base10_parse()?);
110 Ok(())
111 } else if meta.path.is_ident("item_max_graphemes") {
112 let value = meta.value()?;
113 let lit: syn::LitInt = value.parse()?;
114 result.item_max_graphemes = Some(lit.base10_parse()?);
115 Ok(())
116 } else if meta.path.is_ident("item_min_length") {
117 let value = meta.value()?;
118 let lit: syn::LitInt = value.parse()?;
119 result.item_min_length = Some(lit.base10_parse()?);
120 Ok(())
121 } else if meta.path.is_ident("item_min_graphemes") {
122 let value = meta.value()?;
123 let lit: syn::LitInt = value.parse()?;
124 result.item_min_graphemes = Some(lit.base10_parse()?);
125 Ok(())
126 } else if meta.path.is_ident("ref") {
127 let value = meta.value()?;
128 let lit: LitStr = value.parse()?;
129 result.explicit_ref = Some(lit.value());
130 Ok(())
131 } else if meta.path.is_ident("format") {
132 let value = meta.value()?;
133 let lit: LitStr = value.parse()?;
134 result.format = Some(lit.value());
135 Ok(())
136 } else if meta.path.is_ident("union") {
137 // Check if there's a value (explicit type name)
138 if meta.input.peek(syn::Token![=]) {
139 let value = meta.value()?;
140 let lit: LitStr = value.parse()?;
141 result.union_type = Some(Some(lit.value()));
142 } else {
143 // Just #[lexicon(union)] - infer from field type
144 result.union_type = Some(None);
145 }
146 Ok(())
147 } else {
148 Err(meta.error("unknown lexicon field attribute"))
149 }
150 })?;
151 }
152
153 Ok(result)
154}
155
156/// Parse serde attributes for a field
157pub fn parse_serde_attrs(attrs: &[Attribute]) -> syn::Result<SerdeAttrs> {
158 let mut result = SerdeAttrs::default();
159
160 for attr in attrs {
161 if !attr.path().is_ident("serde") {
162 continue;
163 }
164
165 // Use parse_args instead of parse_nested_meta to avoid consuming unknown attributes incorrectly
166 let _ = attr.parse_nested_meta(|meta| {
167 if meta.path.is_ident("rename") {
168 let value = meta.value()?;
169 let lit: LitStr = value.parse()?;
170 result.rename = Some(lit.value());
171 Ok(())
172 } else if meta.path.is_ident("skip") {
173 result.skip = true;
174 Ok(())
175 } else {
176 // Skip unknown attributes - but we need to consume the value if present
177 if meta.input.peek(syn::Token![=]) {
178 let _ = meta.value()?;
179 let _ = meta.input.parse::<proc_macro2::TokenTree>();
180 }
181 Ok(())
182 }
183 });
184 }
185
186 Ok(result)
187}
188
189/// Parse container-level serde rename_all
190pub fn parse_serde_rename_all(attrs: &[Attribute]) -> syn::Result<Option<RenameRule>> {
191 for attr in attrs {
192 if !attr.path().is_ident("serde") {
193 continue;
194 }
195
196 let mut found_rule = None;
197 attr.parse_nested_meta(|meta| {
198 if meta.path.is_ident("rename_all") {
199 let value = meta.value()?;
200 let lit: LitStr = value.parse()?;
201 found_rule = RenameRule::from_str(&lit.value());
202 Ok(())
203 } else {
204 Ok(())
205 }
206 })?;
207
208 if found_rule.is_some() {
209 return Ok(found_rule);
210 }
211 }
212
213 // Default to camelCase (lexicon standard)
214 Ok(Some(RenameRule::CamelCase))
215}
216
217/// Determine NSID from attributes and context
218pub fn determine_nsid(attrs: &LexiconTypeAttrs, input: &DeriveInput) -> syn::Result<String> {
219 if let Some(nsid) = &attrs.nsid {
220 return Ok(nsid.clone());
221 }
222
223 if attrs.fragment.is_some() {
224 return Err(syn::Error::new_spanned(
225 input,
226 "fragments require explicit nsid or module-level primary type (not yet implemented)",
227 ));
228 }
229
230 // Check for XrpcRequest derive with NSID
231 if let Some(nsid) = extract_xrpc_nsid(&input.attrs)? {
232 return Ok(nsid);
233 }
234
235 Err(syn::Error::new_spanned(
236 input,
237 "missing required `nsid` attribute (use #[lexicon(nsid = \"...\")] or #[xrpc(nsid = \"...\")])",
238 ))
239}
240
241/// Extract NSID from XrpcRequest attributes (cross-derive coordination)
242fn extract_xrpc_nsid(attrs: &[Attribute]) -> syn::Result<Option<String>> {
243 for attr in attrs {
244 if !attr.path().is_ident("xrpc") {
245 continue;
246 }
247
248 let mut nsid = None;
249 attr.parse_nested_meta(|meta| {
250 if meta.path.is_ident("nsid") {
251 let value = meta.value()?;
252 let lit: LitStr = value.parse()?;
253 nsid = Some(lit.value());
254 }
255 Ok(())
256 })?;
257
258 if let Some(nsid) = nsid {
259 return Ok(Some(nsid));
260 }
261 }
262 Ok(None)
263}
264
265/// Extract T from Option<T>, return (type, is_required)
266pub fn extract_option_inner(ty: &syn::Type) -> (&syn::Type, bool) {
267 if let syn::Type::Path(type_path) = ty {
268 if let Some(segment) = type_path.path.segments.last() {
269 if segment.ident == "Option" {
270 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
271 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
272 return (inner, false);
273 }
274 }
275 }
276 }
277 }
278 (ty, true)
279}
280
281/// Check if type has #[open_union] attribute
282pub fn has_open_union_attr(attrs: &[Attribute]) -> bool {
283 attrs.iter().any(|attr| attr.path().is_ident("open_union"))
284}
285
286/// Extract NSID ref for a variant
287pub fn extract_variant_ref(variant: &syn::Variant, base_nsid: &str) -> syn::Result<String> {
288 use heck::ToLowerCamelCase;
289
290 // Priority 1: Check for #[nsid = "..."] attribute
291 for attr in &variant.attrs {
292 if attr.path().is_ident("nsid") {
293 if let syn::Meta::NameValue(meta) = &attr.meta {
294 if let syn::Expr::Lit(expr_lit) = &meta.value {
295 if let syn::Lit::Str(lit_str) = &expr_lit.lit {
296 return Ok(lit_str.value());
297 }
298 }
299 }
300 }
301 }
302
303 // Priority 2: Check for #[serde(rename = "...")] attribute
304 for attr in &variant.attrs {
305 if !attr.path().is_ident("serde") {
306 continue;
307 }
308
309 let mut rename = None;
310 let _ = attr.parse_nested_meta(|meta| {
311 if meta.path.is_ident("rename") {
312 let value = meta.value()?;
313 let lit: LitStr = value.parse()?;
314 rename = Some(lit.value());
315 }
316 Ok(())
317 });
318
319 if let Some(rename) = rename {
320 return Ok(rename);
321 }
322 }
323
324 // Priority 3: Generate fragment ref for unit variants
325 match &variant.fields {
326 syn::Fields::Unit => {
327 let variant_name = variant.ident.to_string().to_lower_camel_case();
328 Ok(format!("{}#{}", base_nsid, variant_name))
329 }
330 syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
331 Err(syn::Error::new_spanned(
332 variant,
333 "union variants with non-primitive types must use #[nsid] or #[serde(rename)] attribute to specify the ref",
334 ))
335 }
336 _ => Err(syn::Error::new_spanned(
337 variant,
338 "union variants must be unit variants or have single unnamed field",
339 )),
340 }
341}