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