facet_macros_parse/lib.rs
1#![warn(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4/// Parse function signature shape
5#[cfg(feature = "function")]
6pub mod function;
7#[cfg(feature = "function")]
8pub use function::*;
9
10pub use unsynn::*;
11
12keyword! {
13 /// The "pub" keyword.
14 pub KPub = "pub";
15 /// The "struct" keyword.
16 pub KStruct = "struct";
17 /// The "enum" keyword.
18 pub KEnum = "enum";
19 /// The "doc" keyword.
20 pub KDoc = "doc";
21 /// The "repr" keyword.
22 pub KRepr = "repr";
23 /// The "crate" keyword.
24 pub KCrate = "crate";
25 /// The "in" keyword.
26 pub KIn = "in";
27 /// The "const" keyword.
28 pub KConst = "const";
29 /// The "where" keyword.
30 pub KWhere = "where";
31 /// The "mut" keyword.
32 pub KMut = "mut";
33 /// The "facet" keyword.
34 pub KFacet = "facet";
35 /// The "sensitive" keyword.
36 pub KSensitive = "sensitive";
37 /// The "invariants" keyword.
38 pub KInvariants = "invariants";
39 /// The "opaque" keyword.
40 pub KOpaque = "opaque";
41 /// The "deny_unknown_fields" keyword.
42 pub KDenyUnknownFields = "deny_unknown_fields";
43 /// The "default" keyword.
44 pub KDefault = "default";
45 /// The "transparent" keyword.
46 pub KTransparent = "transparent";
47 /// The "rename" keyword.
48 pub KRename = "rename";
49 /// The "rename_all" keyword.
50 pub KRenameAll = "rename_all";
51 /// The "flatten" keyword
52 pub KFlatten = "flatten";
53 /// The "child" keyword
54 pub KChild = "child";
55 /// The "skip_serializing" keyword.
56 pub KSkipSerializing = "skip_serializing";
57 /// The "skip_serializing_if" keyword.
58 pub KSkipSerializingIf = "skip_serializing_if";
59 /// The "type_tag" keyword.
60 pub KTypeTag = "type_tag";
61 /// The "deserialize_with" keyword.
62 pub KDeserializeWith = "deserialize_with";
63 /// The "serialize_with" keyword.
64 pub KSerializeWith = "serialize_with";
65}
66
67operator! {
68 /// Represents the '=' operator.
69 pub Eq = "=";
70 /// Represents the ';' operator.
71 pub Semi = ";";
72 /// Represents the apostrophe '\'' operator.
73 pub Apostrophe = "'";
74 /// Represents the double semicolon '::' operator.
75 pub DoubleSemicolon = "::";
76}
77
78/// Parses tokens and groups until `C` is found on the current token tree level.
79pub type VerbatimUntil<C> = Many<Cons<Except<C>, AngleTokenTree>>;
80
81/// Represents a module path, consisting of an optional path separator followed by
82/// a path-separator-delimited sequence of identifiers.
83pub type ModPath = Cons<Option<PathSep>, PathSepDelimited<Ident>>;
84
85/// Represents type bounds, consisting of a colon followed by tokens until
86/// a comma, equals sign, or closing angle bracket is encountered.
87pub type Bounds = Cons<Colon, VerbatimUntil<Either<Comma, Eq, Gt>>>;
88
89unsynn! {
90 /// Parses either a `TokenTree` or `<...>` grouping (which is not a [`Group`] as far as proc-macros
91 /// are concerned).
92 #[derive(Clone)]
93 pub struct AngleTokenTree(
94 #[allow(clippy::type_complexity)] // look,
95 pub Either<Cons<Lt, Vec<Cons<Except<Gt>, AngleTokenTree>>, Gt>, TokenTree>,
96 );
97
98 /// Represents an algebraic data type (ADT) declaration, which can be either a struct or enum.
99 pub enum AdtDecl {
100 /// A struct ADT variant.
101 Struct(Struct),
102 /// An enum ADT variant.
103 Enum(Enum),
104 }
105
106 /// Represents visibility modifiers for items.
107 pub enum Vis {
108 /// `pub(in? crate::foo::bar)`/`pub(in? ::foo::bar)`
109 PubIn(Cons<KPub, ParenthesisGroupContaining<Cons<Option<KIn>, ModPath>>>),
110 /// Public visibility, indicated by the "pub" keyword.
111 Pub(KPub),
112 }
113
114 /// Represents an attribute annotation on a field, typically in the form `#[attr]`.
115 pub struct Attribute {
116 /// The pound sign preceding the attribute.
117 pub _pound: Pound,
118 /// The content of the attribute enclosed in square brackets.
119 pub body: BracketGroupContaining<AttributeInner>,
120 }
121
122 /// Represents the inner content of an attribute annotation.
123 pub enum AttributeInner {
124 /// A facet attribute that can contain specialized metadata.
125 Facet(FacetAttr),
126 /// A documentation attribute typically used for generating documentation.
127 Doc(DocInner),
128 /// A representation attribute that specifies how data should be laid out.
129 Repr(ReprInner),
130 /// Any other attribute represented as a sequence of token trees.
131 Any(Vec<TokenTree>),
132 }
133
134 /// Represents a facet attribute that can contain specialized metadata.
135 pub struct FacetAttr {
136 /// The keyword for the facet attribute.
137 pub _facet: KFacet,
138 /// The inner content of the facet attribute.
139 pub inner: ParenthesisGroupContaining<CommaDelimitedVec<FacetInner>>,
140 }
141
142 /// Represents the inner content of a facet attribute.
143 pub enum FacetInner {
144 /// A sensitive attribute that specifies sensitivity information.
145 Sensitive(KSensitive),
146 /// An invariants attribute that specifies invariants for the type.
147 Invariants(InvariantInner),
148 /// An opaque attribute that specifies opaque information.
149 Opaque(KOpaque),
150 /// A deny_unknown_fields attribute that specifies whether unknown fields are allowed.
151 DenyUnknownFields(KDenyUnknownFields),
152 /// A default attribute with an explicit value (#[facet(default = "myfunc")])
153 DefaultEquals(DefaultEqualsInner),
154 /// A default attribute with no explicit value (#[facet(default)])
155 Default(KDefault),
156 /// A transparent attribute for containers
157 Transparent(KTransparent),
158 /// A rename_all attribute that specifies a case conversion for all fields/variants (#[facet(rename_all = "camelCase")])
159 RenameAll(RenameAllInner),
160 /// A rename attribute that specifies a custom name for a field/variant (#[facet(rename = "custom_name")])
161 Rename(RenameInner),
162 /// A flatten attribute that marks a field to be flattened into the parent structure
163 Flatten(FlattenInner),
164 /// A child attribute that marks a field as a child node
165 Child(ChildInner),
166 /// A skip_serializing attribute that specifies whether a field should be skipped during serialization.
167 SkipSerializing(SkipSerializingInner),
168 /// A skip_serializing_if attribute that specifies a condition for skipping serialization.
169 SkipSerializingIf(SkipSerializingIfInner),
170 /// A type_tag attribute that specifies the identifying tag for self describing formats
171 TypeTag(TypeTagInner),
172 /// A function to define how to deserializize the target
173 DeserializeWith(DeserializeWithInner),
174 /// A function to define how to serializize the target
175 SerializeWith(SerializeWithInner),
176 /// Any other attribute represented as a sequence of token trees.
177 Arbitrary(VerbatimUntil<Comma>),
178 }
179
180 /// Inner value for #[facet(flatten)]
181 pub struct FlattenInner {
182 /// The "flatten" keyword.
183 pub _kw_flatten: KFlatten,
184 }
185
186 /// Inner value for #[facet(child)]
187 pub struct ChildInner {
188 /// The "child" keyword.
189 pub _kw_child: KChild,
190 }
191
192 /// Inner value for #[facet(skip_serializing)]
193 pub struct SkipSerializingInner {
194 /// The "skip_serializing" keyword.
195 pub _kw_skip_serializing: KSkipSerializing,
196 }
197
198 /// Inner value for #[facet(skip_serializing_if = ...)]
199 pub struct SkipSerializingIfInner {
200 /// The "skip_serializing_if" keyword.
201 pub _kw_skip_serializing_if: KSkipSerializingIf,
202 /// The equals sign '='.
203 pub _eq: Eq,
204 /// The conditional expression as verbatim until comma.
205 pub expr: VerbatimUntil<Comma>,
206 }
207
208 /// Inner value for #[facet(type_tag = ...)]
209 pub struct TypeTagInner {
210 /// The "type_tag" keyword.
211 pub _kw_type_tag: KTypeTag,
212 /// The equals sign '='.
213 pub _eq: Eq,
214 /// The value assigned, as a literal string.
215 pub expr: LiteralString,
216 }
217
218 /// Inner value for #[facet(deserialize_with = ...)]
219 pub struct DeserializeWithInner {
220 /// The "deserialize_with" keyword.
221 pub _kw_deserialize_with: KDeserializeWith,
222 /// The equals sign '='.
223 pub _eq: Eq,
224 /// The conditional expression as verbatim until comma.
225 pub expr: VerbatimUntil<Comma>,
226 }
227
228 /// Inner value for #[facet(serialize_with = ...)]
229 pub struct SerializeWithInner {
230 /// The "serialize_with" keyword.
231 pub _kw_serialize_with: KSerializeWith,
232 /// The equals sign '='.
233 pub _eq: Eq,
234 /// The conditional expression as verbatim until comma.
235 pub expr: VerbatimUntil<Comma>,
236 }
237
238 /// Inner value for #[facet(default = ...)]
239 pub struct DefaultEqualsInner {
240 /// The "default" keyword.
241 pub _kw_default: KDefault,
242 /// The equals sign '='.
243 pub _eq: Eq,
244 /// The value assigned, as verbatim until comma.
245 pub expr: VerbatimUntil<Comma>,
246 }
247
248 /// Inner value for #[facet(rename = ...)]
249 pub struct RenameInner {
250 /// The "rename" keyword.
251 pub _kw_rename: KRename,
252 /// The equals sign '='.
253 pub _eq: Eq,
254 /// The value assigned, as a literal string.
255 pub value: LiteralString,
256 }
257
258 /// Inner value for #[facet(rename_all = ...)]
259 pub struct RenameAllInner {
260 /// The "rename_all" keyword.
261 pub _kw_rename_all: KRenameAll,
262 /// The equals sign '='.
263 pub _eq: Eq,
264 /// The value assigned, as a literal string.
265 pub value: LiteralString,
266 }
267
268 /// Represents invariants for a type.
269 pub struct InvariantInner {
270 /// The "invariants" keyword.
271 pub _kw_invariants: KInvariants,
272 /// The equality operator.
273 pub _eq: Eq,
274 /// The invariant value
275 pub expr: VerbatimUntil<Comma>,
276 }
277
278 /// Represents documentation for an item.
279 pub struct DocInner {
280 /// The "doc" keyword.
281 pub _kw_doc: KDoc,
282 /// The equality operator.
283 pub _eq: Eq,
284 /// The documentation content as a literal string.
285 pub value: LiteralString,
286 }
287
288 /// Represents the inner content of a `repr` attribute, typically used for specifying
289 /// memory layout or representation hints.
290 pub struct ReprInner {
291 /// The "repr" keyword.
292 pub _kw_repr: KRepr,
293 /// The representation attributes enclosed in parentheses.
294 pub attr: ParenthesisGroupContaining<CommaDelimitedVec<Ident>>,
295 }
296
297 /// Represents a struct definition.
298 pub struct Struct {
299 /// Attributes applied to the struct.
300 pub attributes: Vec<Attribute>,
301 /// The visibility modifier of the struct (e.g., `pub`).
302 pub _vis: Option<Vis>,
303 /// The "struct" keyword.
304 pub _kw_struct: KStruct,
305 /// The name of the struct.
306 pub name: Ident,
307 /// Generic parameters for the struct, if any.
308 pub generics: Option<GenericParams>,
309 /// The variant of struct (unit, tuple, or regular struct with named fields).
310 pub kind: StructKind,
311 }
312
313 /// Represents the generic parameters of a struct or enum definition, enclosed in angle brackets.
314 /// e.g., `<'a, T: Trait, const N: usize>`.
315 pub struct GenericParams {
316 /// The opening angle bracket `<`.
317 pub _lt: Lt,
318 /// The comma-delimited list of generic parameters.
319 pub params: CommaDelimitedVec<GenericParam>,
320 /// The closing angle bracket `>`.
321 pub _gt: Gt,
322 }
323
324 /// Represents a single generic parameter within a `GenericParams` list.
325 pub enum GenericParam {
326 /// A lifetime parameter, e.g., `'a` or `'a: 'b + 'c`.
327 Lifetime {
328 /// The lifetime identifier (e.g., `'a`).
329 name: Lifetime,
330 /// Optional lifetime bounds (e.g., `: 'b + 'c`).
331 bounds: Option<Cons<Colon, VerbatimUntil<Either<Comma, Gt>>>>,
332 },
333 /// A const generic parameter, e.g., `const N: usize = 10`.
334 Const {
335 /// The `const` keyword.
336 _const: KConst,
337 /// The name of the const parameter (e.g., `N`).
338 name: Ident,
339 /// The colon separating the name and type.
340 _colon: Colon,
341 /// The type of the const parameter (e.g., `usize`).
342 typ: VerbatimUntil<Either<Comma, Gt, Eq>>,
343 /// An optional default value (e.g., `= 10`).
344 default: Option<Cons<Eq, VerbatimUntil<Either<Comma, Gt>>>>,
345 },
346 /// A type parameter, e.g., `T: Trait = DefaultType`.
347 Type {
348 /// The name of the type parameter (e.g., `T`).
349 name: Ident,
350 /// Optional type bounds (e.g., `: Trait`).
351 bounds: Option<Bounds>,
352 /// An optional default type (e.g., `= DefaultType`).
353 default: Option<Cons<Eq, VerbatimUntil<Either<Comma, Gt>>>>,
354 },
355 }
356
357 /// Represents a `where` clause attached to a definition.
358 /// e.g., `where T: Trait, 'a: 'b`.
359 #[derive(Clone)]
360 pub struct WhereClauses {
361 /// The `where` keyword.
362 pub _kw_where: KWhere,
363 /// The comma-delimited list of where clause predicates.
364 pub clauses: CommaDelimitedVec<WhereClause>,
365 }
366
367 /// Represents a single predicate within a `where` clause.
368 /// e.g., `T: Trait` or `'a: 'b`.
369 #[derive(Clone)]
370 pub struct WhereClause {
371 /// The type or lifetime being constrained (e.g., `T` or `'a`).
372 /// We specifically required a single colon, not 2 because of `<A as B>::Thingy`
373 pub _pred: VerbatimUntil<Cons<Colon, Except<Colon>>>,
374 /// The colon separating the constrained item and its bounds.
375 pub _colon: Colon,
376 /// The bounds applied to the type or lifetime (e.g., `Trait` or `'b`).
377 pub bounds: VerbatimUntil<Either<Comma, Semicolon, BraceGroup>>,
378 }
379
380 /// Represents the kind of a struct definition.
381 pub enum StructKind {
382 /// A regular struct with named fields, e.g., `struct Foo { bar: u32 }`.
383 Struct {
384 /// Optional where clauses.
385 clauses: Option<WhereClauses>,
386 /// The fields enclosed in braces `{}`.
387 fields: BraceGroupContaining<CommaDelimitedVec<StructField>>,
388 },
389 /// A tuple struct, e.g., `struct Foo(u32, String);`.
390 TupleStruct {
391 /// The fields enclosed in parentheses `()`.
392 fields: ParenthesisGroupContaining<CommaDelimitedVec<TupleField>>,
393 /// Optional where clauses.
394 clauses: Option<WhereClauses>,
395 /// The trailing semicolon `;`.
396 semi: Semi,
397 },
398 /// A unit struct, e.g., `struct Foo;`.
399 UnitStruct {
400 /// Optional where clauses.
401 clauses: Option<WhereClauses>,
402 /// The trailing semicolon `;`.
403 semi: Semi,
404 },
405 }
406
407 /// Represents a lifetime annotation, like `'a`.
408 pub struct Lifetime {
409 /// The apostrophe `'` starting the lifetime.
410 pub _apostrophe: PunctJoint<'\''>,
411 /// The identifier name of the lifetime (e.g., `a`).
412 pub name: Ident,
413 }
414
415 /// Represents a simple expression, currently only integer literals.
416 /// Used potentially for const generic default values.
417 pub enum Expr {
418 /// An integer literal expression.
419 Integer(LiteralInteger),
420 }
421
422 /// Represents either the `const` or `mut` keyword, often used with pointers.
423 pub enum ConstOrMut {
424 /// The `const` keyword.
425 Const(KConst),
426 /// The `mut` keyword.
427 Mut(KMut),
428 }
429
430 /// Represents a field within a regular struct definition.
431 /// e.g., `pub name: String`.
432 pub struct StructField {
433 /// Attributes applied to the field (e.g., `#[doc = "..."]`).
434 pub attributes: Vec<Attribute>,
435 /// Optional visibility modifier (e.g., `pub`).
436 pub _vis: Option<Vis>,
437 /// The name of the field.
438 pub name: Ident,
439 /// The colon separating the name and type.
440 pub _colon: Colon,
441 /// The type of the field.
442 pub typ: VerbatimUntil<Comma>,
443 }
444
445 /// Represents a field within a tuple struct definition.
446 /// e.g., `pub String`.
447 pub struct TupleField {
448 /// Attributes applied to the field (e.g., `#[doc = "..."]`).
449 pub attributes: Vec<Attribute>,
450 /// Optional visibility modifier (e.g., `pub`).
451 pub vis: Option<Vis>,
452 /// The type of the field.
453 pub typ: VerbatimUntil<Comma>,
454 }
455
456 /// Represents an enum definition.
457 /// e.g., `#[repr(u8)] pub enum MyEnum<T> where T: Clone { Variant1, Variant2(T) }`.
458 pub struct Enum {
459 /// Attributes applied to the enum (e.g., `#[repr(...)]`).
460 pub attributes: Vec<Attribute>,
461 /// Optional visibility modifier (e.g., `pub`, `pub(crate)`, etc.).
462 pub _vis: Option<Vis>,
463 /// The `enum` keyword.
464 pub _kw_enum: KEnum,
465 /// The name of the enum.
466 pub name: Ident,
467 /// Optional generic parameters.
468 pub generics: Option<GenericParams>,
469 /// Optional where clauses.
470 pub clauses: Option<WhereClauses>,
471 /// The enum variants enclosed in braces `{}`.
472 pub body: BraceGroupContaining<CommaDelimitedVec<EnumVariantLike>>,
473 }
474
475 /// Represents a variant of an enum, including the optional discriminant value
476 pub struct EnumVariantLike {
477 /// The actual variant
478 pub variant: EnumVariantData,
479 /// The optional discriminant value
480 pub discriminant: Option<Cons<Eq, VerbatimUntil<Comma>>>
481 }
482
483 /// Represents the different kinds of variants an enum can have.
484 pub enum EnumVariantData {
485 /// A tuple-like variant, e.g., `Variant(u32, String)`.
486 Tuple(TupleVariant),
487 /// A struct-like variant, e.g., `Variant { field1: u32, field2: String }`.
488 Struct(StructEnumVariant),
489 /// A unit-like variant, e.g., `Variant`.
490 Unit(UnitVariant),
491 }
492
493 /// Represents a unit-like enum variant.
494 /// e.g., `MyVariant`.
495 pub struct UnitVariant {
496 /// Attributes applied to the variant.
497 pub attributes: Vec<Attribute>,
498 /// The name of the variant.
499 pub name: Ident,
500 }
501
502 /// Represents a tuple-like enum variant.
503 /// e.g., `MyVariant(u32, String)`.
504 pub struct TupleVariant {
505 /// Attributes applied to the variant.
506 pub attributes: Vec<Attribute>,
507 /// The name of the variant.
508 pub name: Ident,
509 /// The fields enclosed in parentheses `()`.
510 pub fields: ParenthesisGroupContaining<CommaDelimitedVec<TupleField>>,
511 }
512
513 /// Represents a struct-like enum variant.
514 /// e.g., `MyVariant { field1: u32, field2: String }`.
515 pub struct StructEnumVariant {
516 /// Attributes applied to the variant.
517 pub attributes: Vec<Attribute>,
518 /// The name of the variant.
519 pub name: Ident,
520 /// The fields enclosed in braces `{}`.
521 pub fields: BraceGroupContaining<CommaDelimitedVec<StructField>>,
522 }
523
524 /// A lifetime or a tokentree, used to gather lifetimes in type definitions
525 pub enum LifetimeOrTt {
526 /// A lifetime annotation.
527 Lifetime(Lifetime),
528 /// A single token tree.
529 TokenTree(TokenTree),
530 }
531}
532
533impl core::fmt::Display for AngleTokenTree {
534 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
535 match &self.0 {
536 Either::First(it) => {
537 write!(f, "<")?;
538 for it in it.second.iter() {
539 write!(f, "{}", it.second)?;
540 }
541 write!(f, ">")?;
542 }
543 Either::Second(it) => write!(f, "{it}")?,
544 Either::Third(Invalid) => unreachable!(),
545 Either::Fourth(Invalid) => unreachable!(),
546 };
547 Ok(())
548 }
549}
550
551/// Display the verbatim tokens until the given token.
552pub struct VerbatimDisplay<'a, C>(pub &'a VerbatimUntil<C>);
553
554impl<C> core::fmt::Display for VerbatimDisplay<'_, C> {
555 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
556 for tt in self.0.iter() {
557 write!(f, "{}", tt.value.second)?;
558 }
559 Ok(())
560 }
561}
562
563impl core::fmt::Display for ConstOrMut {
564 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
565 match self {
566 ConstOrMut::Const(_) => write!(f, "const"),
567 ConstOrMut::Mut(_) => write!(f, "mut"),
568 }
569 }
570}
571
572impl core::fmt::Display for Lifetime {
573 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
574 write!(f, "'{}", self.name)
575 }
576}
577
578impl core::fmt::Display for WhereClauses {
579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580 write!(f, "where ")?;
581 for clause in self.clauses.iter() {
582 write!(f, "{},", clause.value)?;
583 }
584 Ok(())
585 }
586}
587
588impl core::fmt::Display for WhereClause {
589 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590 write!(
591 f,
592 "{}: {}",
593 VerbatimDisplay(&self._pred),
594 VerbatimDisplay(&self.bounds)
595 )
596 }
597}
598
599impl core::fmt::Display for Expr {
600 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
601 match self {
602 Expr::Integer(int) => write!(f, "{}", int.value()),
603 }
604 }
605}
606
607impl Struct {
608 /// Returns an iterator over the `FacetInner` content of `#[facet(...)]` attributes
609 /// applied to this struct.
610 pub fn facet_attributes(&self) -> impl Iterator<Item = &FacetInner> {
611 self.attributes
612 .iter()
613 .filter_map(|attr| match &attr.body.content {
614 AttributeInner::Facet(f) => Some(f.inner.content.as_slice()),
615 _ => None,
616 })
617 .flatten()
618 .map(|d| &d.value)
619 }
620
621 /// Returns `true` if the struct is marked `#[facet(transparent)]`.
622 pub fn is_transparent(&self) -> bool {
623 self.facet_attributes()
624 .any(|inner| matches!(inner, FacetInner::Transparent(_)))
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631 use quote::quote;
632
633 #[test]
634 fn test_struct_with_field_doc_comments() {
635 let input = quote! {
636 #[derive(Facet)]
637 pub struct User {
638 #[doc = " The user's unique identifier"]
639 pub id: u64,
640 }
641 };
642
643 let mut it = input.to_token_iter();
644 let parsed = it.parse::<Struct>().expect("Failed to parse struct");
645
646 // Check that we parsed the struct correctly
647 assert_eq!(parsed.name.to_string(), "User");
648
649 // Extract fields from the struct
650 if let StructKind::Struct { fields, .. } = &parsed.kind {
651 assert_eq!(fields.content.len(), 1);
652
653 // Check first field (id)
654 let id_field = &fields.content[0].value;
655 assert_eq!(id_field.name.to_string(), "id");
656
657 // Extract doc comments from id field
658 let mut doc_found = false;
659 for attr in &id_field.attributes {
660 match &attr.body.content {
661 AttributeInner::Doc(doc_inner) => {
662 // This should work with LiteralString
663 assert_eq!(doc_inner.value, " The user's unique identifier");
664 doc_found = true;
665 }
666 _ => {
667 // Skip non-doc attributes
668 }
669 }
670 }
671 assert!(doc_found, "Should have found a doc comment");
672 } else {
673 panic!("Expected a regular struct with named fields");
674 }
675 }
676}