1use syn::Meta;
19
20use crate::path::{MetaPath, Segment};
21
22pub trait MetaExt {
41 fn is_path(&self) -> bool;
43 fn is_list(&self) -> bool;
45 fn is_name_value(&self) -> bool;
47 fn as_path(&self) -> Option<&syn::Path>;
49 fn as_list(&self) -> Option<&syn::MetaList>;
51 fn as_name_value(&self) -> Option<&syn::MetaNameValue>;
53 fn is(&self, name: &str) -> bool;
55 fn get(&self, path: &str) -> Option<Meta>;
69 fn nested(&self) -> Option<Vec<Meta>>;
82 fn span(&self) -> proc_macro2::Span;
84}
85
86impl MetaExt for Meta {
87 fn is_path(&self) -> bool {
88 matches!(self, Self::Path(_))
89 }
90
91 fn is_list(&self) -> bool {
92 matches!(self, Self::List(_))
93 }
94
95 fn is_name_value(&self) -> bool {
96 matches!(self, Self::NameValue(_))
97 }
98
99 fn as_path(&self) -> Option<&syn::Path> {
100 match self {
101 Self::Path(p) => Some(p),
102 _ => None,
103 }
104 }
105
106 fn as_list(&self) -> Option<&syn::MetaList> {
107 match self {
108 Self::List(l) => Some(l),
109 _ => None,
110 }
111 }
112
113 fn as_name_value(&self) -> Option<&syn::MetaNameValue> {
114 match self {
115 Self::NameValue(nv) => Some(nv),
116 _ => None,
117 }
118 }
119
120 fn is(&self, name: &str) -> bool {
121 self.path().is_ident(name)
122 }
123
124 fn get(&self, path: &str) -> Option<Meta> {
125 let parsed = MetaPath::parse(path).ok()?;
126 self.resolve(&parsed)
127 }
128
129 fn nested(&self) -> Option<Vec<Meta>> {
130 let list = match self {
131 Self::List(list) => list,
132 _ => return None,
133 };
134
135 list.parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
136 .ok()
137 .map(|p| p.into_iter().collect())
138 }
139
140 fn span(&self) -> proc_macro2::Span {
141 use syn::spanned::Spanned;
142 Spanned::span(self)
143 }
144}
145
146trait MetaExtPrivate {
147 fn resolve(&self, path: &MetaPath) -> Option<Meta>;
148}
149
150impl MetaExtPrivate for Meta {
151 fn resolve(&self, path: &MetaPath) -> Option<Meta> {
152 let seg = path.first()?;
153 let tail = path.tail();
154
155 let list = match self {
156 Self::List(list) => list,
157 _ => return None,
158 };
159
160 let nested: Vec<Meta> = list
161 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
162 .ok()?
163 .into_iter()
164 .collect();
165
166 let found = match seg {
167 Segment::Key(name) => nested.iter().find(|m| m.path().is_ident(name))?.clone(),
168 Segment::Index(i) => nested.get(*i)?.clone(),
169 };
170
171 if tail.is_empty() {
172 Some(found)
173 } else {
174 found.resolve(&tail)
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn meta_from(attr_str: &str) -> Meta {
184 let item: syn::ItemStruct = syn::parse_str(&format!("{} struct Foo;", attr_str)).unwrap();
185 item.attrs.into_iter().next().unwrap().meta
186 }
187
188 mod predicates {
189 use super::*;
190
191 #[test]
192 fn path_variant() {
193 let meta = meta_from("#[test]");
194 assert!(meta.is_path());
195 assert!(!meta.is_list());
196 assert!(!meta.is_name_value());
197 }
198
199 #[test]
200 fn list_variant() {
201 let meta = meta_from("#[derive(Clone)]");
202 assert!(!meta.is_path());
203 assert!(meta.is_list());
204 assert!(!meta.is_name_value());
205 }
206
207 #[test]
208 fn name_value_variant() {
209 let meta = meta_from("#[path = \"foo\"]");
210 assert!(!meta.is_path());
211 assert!(!meta.is_list());
212 assert!(meta.is_name_value());
213 }
214 }
215
216 mod conversions {
217 use super::*;
218
219 #[test]
220 fn as_path_some() {
221 let meta = meta_from("#[test]");
222 assert!(meta.as_path().is_some());
223 }
224
225 #[test]
226 fn as_path_none() {
227 let meta = meta_from("#[derive(Clone)]");
228 assert!(meta.as_path().is_none());
229 }
230
231 #[test]
232 fn as_list_some() {
233 let meta = meta_from("#[derive(Clone)]");
234 assert!(meta.as_list().is_some());
235 }
236
237 #[test]
238 fn as_list_none() {
239 let meta = meta_from("#[test]");
240 assert!(meta.as_list().is_none());
241 }
242
243 #[test]
244 fn as_name_value_some() {
245 let meta = meta_from("#[path = \"foo\"]");
246 assert!(meta.as_name_value().is_some());
247 }
248
249 #[test]
250 fn as_name_value_none() {
251 let meta = meta_from("#[test]");
252 assert!(meta.as_name_value().is_none());
253 }
254 }
255
256 mod is {
257 use super::*;
258
259 #[test]
260 fn matching_name() {
261 let meta = meta_from("#[serde(skip)]");
262 assert!(meta.is("serde"));
263 }
264
265 #[test]
266 fn non_matching_name() {
267 let meta = meta_from("#[serde(skip)]");
268 assert!(!meta.is("derive"));
269 }
270 }
271
272 mod get {
273 use super::*;
274
275 #[test]
276 fn find_by_key() {
277 let meta = meta_from("#[serde(rename = \"id\", skip)]");
278 let found = meta.get("skip").unwrap();
279 assert!(found.is_path());
280 }
281
282 #[test]
283 fn find_name_value() {
284 let meta = meta_from("#[serde(rename = \"id\")]");
285 let found = meta.get("rename").unwrap();
286 assert!(found.is_name_value());
287 }
288
289 #[test]
290 fn find_by_index() {
291 let meta = meta_from("#[derive(Clone, Debug)]");
292 let found = meta.get("[1]").unwrap();
293 assert!(found.path().is_ident("Debug"));
294 }
295
296 #[test]
297 fn missing_key() {
298 let meta = meta_from("#[serde(skip)]");
299 assert!(meta.get("rename").is_none());
300 }
301
302 #[test]
303 fn get_on_path_variant() {
304 let meta = meta_from("#[test]");
305 assert!(meta.get("anything").is_none());
306 }
307
308 #[test]
309 fn deep_nested_key_path() {
310 let meta = meta_from("#[cfg(all(feature = \"a\", feature = \"b\"))]");
311 let all = meta.get("all").unwrap();
312 assert!(all.is_list());
313 }
314
315 #[test]
316 fn deep_nested_dot_path() {
317 let meta = meta_from("#[cfg(all(feature = \"a\", feature = \"b\"))]");
318 let feature = meta.get("all.feature").unwrap();
319 assert!(feature.is_name_value());
320 }
321
322 #[test]
323 fn deep_nested_index_path() {
324 let meta = meta_from("#[cfg(all(feature = \"a\", target_os = \"linux\"))]");
325 let second = meta.get("all.[1]").unwrap();
326 assert!(second.is_name_value());
327 assert!(second.path().is_ident("target_os"));
328 }
329
330 #[test]
331 fn three_levels_deep() {
332 let meta = meta_from("#[cfg(all(not(feature = \"a\")))]");
333 let not = meta.get("all.not").unwrap();
334 assert!(not.is_list());
335 let feature = meta.get("all.not.feature").unwrap();
336 assert!(feature.is_name_value());
337 }
338
339 #[test]
340 fn deep_missing_intermediate() {
341 let meta = meta_from("#[cfg(all(feature = \"a\"))]");
342 assert!(meta.get("all.nonexistent.feature").is_none());
343 }
344
345 #[test]
346 fn deep_index_out_of_bounds() {
347 let meta = meta_from("#[cfg(all(feature = \"a\"))]");
348 assert!(meta.get("all.[5]").is_none());
349 }
350
351 #[test]
352 fn deep_index_then_key() {
353 let meta = meta_from("#[cfg(all(not(feature = \"a\"), target_os = \"linux\"))]");
354 let feature = meta.get("all.[0].feature").unwrap();
355 assert!(feature.is_name_value());
356 }
357 }
358
359 mod nested {
360 use super::*;
361
362 #[test]
363 fn list_returns_items() {
364 let meta = meta_from("#[derive(Clone, Debug)]");
365 let items = meta.nested().unwrap();
366 assert_eq!(items.len(), 2);
367 }
368
369 #[test]
370 fn path_returns_none() {
371 let meta = meta_from("#[test]");
372 assert!(meta.nested().is_none());
373 }
374
375 #[test]
376 fn name_value_returns_none() {
377 let meta = meta_from("#[path = \"foo\"]");
378 assert!(meta.nested().is_none());
379 }
380 }
381}