@@ -3,6 +3,33 @@ use std::collections::{BTreeMap, HashMap};
33use syn:: { Fields , Type } ;
44use vespera_core:: schema:: { Reference , Schema , SchemaRef , SchemaType } ;
55
6+ /// Extract doc comments from attributes.
7+ /// Returns concatenated doc comment string or None if no doc comments.
8+ pub fn extract_doc_comment ( attrs : & [ syn:: Attribute ] ) -> Option < String > {
9+ let mut doc_lines = Vec :: new ( ) ;
10+
11+ for attr in attrs {
12+ if attr. path ( ) . is_ident ( "doc" )
13+ && let syn:: Meta :: NameValue ( meta_nv) = & attr. meta
14+ && let syn:: Expr :: Lit ( syn:: ExprLit {
15+ lit : syn:: Lit :: Str ( lit_str) ,
16+ ..
17+ } ) = & meta_nv. value
18+ {
19+ let line = lit_str. value ( ) ;
20+ // Trim leading space that rustdoc adds
21+ let trimmed = line. strip_prefix ( ' ' ) . unwrap_or ( & line) ;
22+ doc_lines. push ( trimmed. to_string ( ) ) ;
23+ }
24+ }
25+
26+ if doc_lines. is_empty ( ) {
27+ None
28+ } else {
29+ Some ( doc_lines. join ( "\n " ) )
30+ }
31+ }
32+
633/// Strips the `r#` prefix from raw identifiers.
734/// E.g., `r#type` becomes `type`.
835pub fn strip_raw_prefix ( ident : & str ) -> & str {
@@ -437,6 +464,9 @@ pub fn parse_enum_to_schema(
437464 known_schemas : & HashMap < String , String > ,
438465 struct_definitions : & HashMap < String , String > ,
439466) -> Schema {
467+ // Extract enum-level doc comment for schema description
468+ let enum_description = extract_doc_comment ( & enum_item. attrs ) ;
469+
440470 // Extract rename_all attribute from enum
441471 let rename_all = extract_rename_all ( & enum_item. attrs ) ;
442472
@@ -466,6 +496,7 @@ pub fn parse_enum_to_schema(
466496
467497 Schema {
468498 schema_type : Some ( SchemaType :: String ) ,
499+ description : enum_description,
469500 r#enum : if enum_values. is_empty ( ) {
470501 None
471502 } else {
@@ -488,10 +519,14 @@ pub fn parse_enum_to_schema(
488519 rename_field ( & variant_name, rename_all. as_deref ( ) )
489520 } ;
490521
522+ // Extract variant-level doc comment
523+ let variant_description = extract_doc_comment ( & variant. attrs ) ;
524+
491525 let variant_schema = match & variant. fields {
492526 syn:: Fields :: Unit => {
493527 // Unit variant: {"const": "VariantName"}
494528 Schema {
529+ description : variant_description,
495530 r#enum : Some ( vec ! [ serde_json:: Value :: String ( variant_key) ] ) ,
496531 ..Schema :: string ( )
497532 }
@@ -510,6 +545,7 @@ pub fn parse_enum_to_schema(
510545 properties. insert ( variant_key. clone ( ) , inner_schema) ;
511546
512547 Schema {
548+ description : variant_description. clone ( ) ,
513549 properties : Some ( properties) ,
514550 required : Some ( vec ! [ variant_key] ) ,
515551 ..Schema :: object ( )
@@ -546,6 +582,7 @@ pub fn parse_enum_to_schema(
546582 ) ;
547583
548584 Schema {
585+ description : variant_description. clone ( ) ,
549586 properties : Some ( properties) ,
550587 required : Some ( vec ! [ variant_key] ) ,
551588 ..Schema :: object ( )
@@ -577,9 +614,31 @@ pub fn parse_enum_to_schema(
577614 } ;
578615
579616 let field_type = & field. ty ;
580- let schema_ref =
617+ let mut schema_ref =
581618 parse_type_to_schema_ref ( field_type, known_schemas, struct_definitions) ;
582619
620+ // Extract doc comment from field and set as description
621+ if let Some ( doc) = extract_doc_comment ( & field. attrs ) {
622+ match & mut schema_ref {
623+ SchemaRef :: Inline ( schema) => {
624+ schema. description = Some ( doc) ;
625+ }
626+ SchemaRef :: Ref ( _) => {
627+ let ref_schema = std:: mem:: replace (
628+ & mut schema_ref,
629+ SchemaRef :: Inline ( Box :: new ( Schema :: object ( ) ) ) ,
630+ ) ;
631+ if let SchemaRef :: Ref ( reference) = ref_schema {
632+ schema_ref = SchemaRef :: Inline ( Box :: new ( Schema {
633+ description : Some ( doc) ,
634+ all_of : Some ( vec ! [ SchemaRef :: Ref ( reference) ] ) ,
635+ ..Default :: default ( )
636+ } ) ) ;
637+ }
638+ }
639+ }
640+ }
641+
583642 variant_properties. insert ( field_name. clone ( ) , schema_ref) ;
584643
585644 // Check if field is Option<T>
@@ -621,6 +680,7 @@ pub fn parse_enum_to_schema(
621680 ) ;
622681
623682 Schema {
683+ description : variant_description,
624684 properties : Some ( properties) ,
625685 required : Some ( vec ! [ variant_key] ) ,
626686 ..Schema :: object ( )
@@ -633,6 +693,7 @@ pub fn parse_enum_to_schema(
633693
634694 Schema {
635695 schema_type : None , // oneOf doesn't have a single type
696+ description : enum_description,
636697 one_of : if one_of_schemas. is_empty ( ) {
637698 None
638699 } else {
@@ -651,6 +712,9 @@ pub fn parse_struct_to_schema(
651712 let mut properties = BTreeMap :: new ( ) ;
652713 let mut required = Vec :: new ( ) ;
653714
715+ // Extract struct-level doc comment for schema description
716+ let struct_description = extract_doc_comment ( & struct_item. attrs ) ;
717+
654718 // Extract rename_all attribute from struct
655719 let rename_all = extract_rename_all ( & struct_item. attrs ) ;
656720
@@ -681,6 +745,31 @@ pub fn parse_struct_to_schema(
681745 let mut schema_ref =
682746 parse_type_to_schema_ref ( field_type, known_schemas, struct_definitions) ;
683747
748+ // Extract doc comment from field and set as description
749+ if let Some ( doc) = extract_doc_comment ( & field. attrs ) {
750+ match & mut schema_ref {
751+ SchemaRef :: Inline ( schema) => {
752+ schema. description = Some ( doc) ;
753+ }
754+ SchemaRef :: Ref ( _) => {
755+ // For $ref schemas, we need to wrap in an allOf to add description
756+ // OpenAPI 3.1 allows siblings to $ref, so we can add description directly
757+ // by converting to inline schema with description + allOf[$ref]
758+ let ref_schema = std:: mem:: replace (
759+ & mut schema_ref,
760+ SchemaRef :: Inline ( Box :: new ( Schema :: object ( ) ) ) ,
761+ ) ;
762+ if let SchemaRef :: Ref ( reference) = ref_schema {
763+ schema_ref = SchemaRef :: Inline ( Box :: new ( Schema {
764+ description : Some ( doc) ,
765+ all_of : Some ( vec ! [ SchemaRef :: Ref ( reference) ] ) ,
766+ ..Default :: default ( )
767+ } ) ) ;
768+ }
769+ }
770+ }
771+ }
772+
684773 // Check for default attribute
685774 let has_default = extract_default ( & field. attrs ) . is_some ( ) ;
686775
@@ -727,6 +816,7 @@ pub fn parse_struct_to_schema(
727816
728817 Schema {
729818 schema_type : Some ( SchemaType :: Object ) ,
819+ description : struct_description,
730820 properties : if properties. is_empty ( ) {
731821 None
732822 } else {
0 commit comments