77//! This module facilitates the transformation of `ParsedStruct` and `ParsedEnum` definitions
88//! into valid, compilable Rust code. It handles:
99//! - Dependency analysis (auto-injecting imports like `Uuid`, `chrono`, `serde`).
10- //! - Attribute injection (`derive`, `serde` options).
11- //! - Formatting and comments preservation.
10+ //! - Attribute injection (`derive`, `serde` options, `deprecated` ).
11+ //! - Formatting and comments preservation including external docs links .
1212
1313use crate :: error:: { AppError , AppResult } ;
14+ use crate :: parser:: models:: ParsedExternalDocs ;
1415use crate :: parser:: { ParsedEnum , ParsedModel , ParsedStruct } ;
1516use ra_ap_edition:: Edition ;
1617use ra_ap_syntax:: { ast, AstNode , SourceFile } ;
@@ -104,15 +105,43 @@ pub fn generate_dto(dto: &ParsedStruct) -> String {
104105 generate_dtos ( & [ ParsedModel :: Struct ( dto. clone ( ) ) ] )
105106}
106107
108+ /// Helper to generate documentation string with optional external docs link.
109+ fn generate_doc_comment (
110+ description : Option < & String > ,
111+ external : Option < & ParsedExternalDocs > ,
112+ indent : & str ,
113+ ) -> String {
114+ let mut code = String :: new ( ) ;
115+ if let Some ( desc) = description {
116+ for line in desc. lines ( ) {
117+ code. push_str ( & format ! ( "{}/// {}\n " , indent, line) ) ;
118+ }
119+ }
120+
121+ if let Some ( ext) = external {
122+ if !code. is_empty ( ) {
123+ code. push_str ( & format ! ( "{}///\n " , indent) ) ;
124+ }
125+ let desc = ext. description . as_deref ( ) . unwrap_or ( "See also" ) ;
126+ code. push_str ( & format ! ( "{}/// {}: <{}>\n " , indent, desc, ext. url) ) ;
127+ }
128+ code
129+ }
130+
107131/// Helper to generate the body of a single struct (without file-level imports).
108132fn generate_dto_body ( dto : & ParsedStruct ) -> String {
109133 let mut code = String :: new ( ) ;
110134
111135 // Docs
112- if let Some ( desc) = & dto. description {
113- for line in desc. lines ( ) {
114- code. push_str ( & format ! ( "/// {}\n " , line) ) ;
115- }
136+ code. push_str ( & generate_doc_comment (
137+ dto. description . as_ref ( ) ,
138+ dto. external_docs . as_ref ( ) ,
139+ "" ,
140+ ) ) ;
141+
142+ // Deprecated
143+ if dto. is_deprecated {
144+ code. push_str ( "#[deprecated]\n " ) ;
116145 }
117146
118147 // Derives
@@ -127,13 +156,18 @@ fn generate_dto_body(dto: &ParsedStruct) -> String {
127156
128157 for field in & dto. fields {
129158 // Field Docs
130- if let Some ( field_desc) = & field. description {
131- for line in field_desc. lines ( ) {
132- code. push_str ( & format ! ( " /// {}\n " , line) ) ;
133- }
159+ code. push_str ( & generate_doc_comment (
160+ field. description . as_ref ( ) ,
161+ field. external_docs . as_ref ( ) ,
162+ " " ,
163+ ) ) ;
164+
165+ // Field Deprecated
166+ if field. is_deprecated {
167+ code. push_str ( " #[deprecated]\n " ) ;
134168 }
135169
136- // Field Attributes (Rename/Skip)
170+ // Field Attributes (Rename/Skip/Flatten )
137171 let mut attrs = Vec :: new ( ) ;
138172 if let Some ( rename) = & field. rename {
139173 attrs. push ( format ! ( "rename = \" {}\" " , rename) ) ;
@@ -142,6 +176,11 @@ fn generate_dto_body(dto: &ParsedStruct) -> String {
142176 attrs. push ( "skip" . to_string ( ) ) ;
143177 }
144178
179+ // Handle Map Flattening
180+ if field. name == "additional_properties" && field. ty . contains ( "HashMap" ) {
181+ attrs. push ( "flatten" . to_string ( ) ) ;
182+ }
183+
145184 if !attrs. is_empty ( ) {
146185 code. push_str ( & format ! ( " #[serde({})]\n " , attrs. join( ", " ) ) ) ;
147186 }
@@ -158,10 +197,14 @@ fn generate_dto_body(dto: &ParsedStruct) -> String {
158197fn generate_enum_body ( en : & ParsedEnum ) -> String {
159198 let mut code = String :: new ( ) ;
160199
161- if let Some ( desc) = & en. description {
162- for line in desc. lines ( ) {
163- code. push_str ( & format ! ( "/// {}\n " , line) ) ;
164- }
200+ code. push_str ( & generate_doc_comment (
201+ en. description . as_ref ( ) ,
202+ en. external_docs . as_ref ( ) ,
203+ "" ,
204+ ) ) ;
205+
206+ if en. is_deprecated {
207+ code. push_str ( "#[deprecated]\n " ) ;
165208 }
166209
167210 code. push_str ( "#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]\n " ) ;
@@ -191,6 +234,10 @@ fn generate_enum_body(en: &ParsedEnum) -> String {
191234 }
192235 }
193236
237+ if variant. is_deprecated {
238+ code. push_str ( " #[deprecated]\n " ) ;
239+ }
240+
194241 if let Some ( r) = & variant. rename {
195242 code. push_str ( & format ! ( " #[serde(rename = \" {}\" )]\n " , r) ) ;
196243 }
@@ -230,19 +277,22 @@ fn collect_imports(model: &ParsedModel, imports: &mut BTreeSet<String>) {
230277 if ty. contains ( "NaiveDate" ) && !ty. contains ( "NaiveDateTime" ) {
231278 imports. insert ( "use chrono::NaiveDate;" . to_string ( ) ) ;
232279 }
233- if ty. contains ( "Value" ) {
280+ if ty. contains ( "Value" ) || ty . contains ( "serde_json" ) {
234281 imports. insert ( "use serde_json::Value;" . to_string ( ) ) ;
235282 }
236283 if ty. contains ( "Decimal" ) {
237284 imports. insert ( "use rust_decimal::Decimal;" . to_string ( ) ) ;
238285 }
286+ if ty. contains ( "HashMap" ) {
287+ imports. insert ( "use std::collections::HashMap;" . to_string ( ) ) ;
288+ }
239289 }
240290}
241291
242292#[ cfg( test) ]
243293mod tests {
244294 use super :: * ;
245- use crate :: parser:: { ParsedField , ParsedVariant } ;
295+ use crate :: parser:: { ParsedExternalDocs , ParsedField , ParsedVariant } ;
246296
247297 fn field ( name : & str , ty : & str ) -> ParsedField {
248298 ParsedField {
@@ -251,6 +301,8 @@ mod tests {
251301 description : None ,
252302 rename : None ,
253303 is_skipped : false ,
304+ is_deprecated : false ,
305+ external_docs : None ,
254306 }
255307 }
256308
@@ -261,6 +313,8 @@ mod tests {
261313 description : Some ( "A simple struct" . into ( ) ) ,
262314 rename : None ,
263315 fields : vec ! [ field( "id" , "i32" ) ] ,
316+ is_deprecated : false ,
317+ external_docs : None ,
264318 } ;
265319
266320 let code = generate_dto ( & dto) ;
@@ -269,6 +323,40 @@ mod tests {
269323 assert ! ( code. contains( "#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]" ) ) ;
270324 }
271325
326+ #[ test]
327+ fn test_generate_dto_with_metadata ( ) {
328+ let docs = ParsedExternalDocs {
329+ url : "https://example.com" . into ( ) ,
330+ description : Some ( "More info" . into ( ) ) ,
331+ } ;
332+
333+ let dto = ParsedStruct {
334+ name : "Oldie" . into ( ) ,
335+ description : Some ( "Desc" . into ( ) ) ,
336+ rename : None ,
337+ fields : vec ! [ ParsedField {
338+ name: "f" . into( ) ,
339+ ty: "i32" . into( ) ,
340+ description: None ,
341+ rename: None ,
342+ is_skipped: false ,
343+ is_deprecated: true ,
344+ external_docs: None ,
345+ } ] ,
346+ is_deprecated : true ,
347+ external_docs : Some ( docs) ,
348+ } ;
349+
350+ let code = generate_dto ( & dto) ;
351+ assert ! ( code. contains( "#[deprecated]" ) ) ;
352+ assert ! ( code. contains( "struct Oldie" ) ) ;
353+ assert ! ( code. contains( "/// Desc" ) ) ;
354+ assert ! ( code. contains( "/// More info: <https://example.com>" ) ) ;
355+ // Check field deprecation
356+ assert ! ( code. contains( " #[deprecated]" ) ) ;
357+ assert ! ( code. contains( " pub f: i32" ) ) ;
358+ }
359+
272360 #[ test]
273361 fn test_generate_enum_tagged ( ) {
274362 let en = ParsedEnum {
@@ -277,20 +365,24 @@ mod tests {
277365 rename : None ,
278366 tag : Some ( "type" . into ( ) ) ,
279367 untagged : false ,
368+ is_deprecated : false ,
369+ external_docs : None ,
280370 variants : vec ! [
281371 ParsedVariant {
282372 name: "Cat" . into( ) ,
283373 ty: Some ( "CatInfo" . into( ) ) ,
284374 description: None ,
285375 rename: Some ( "cat" . into( ) ) ,
286376 aliases: Some ( vec![ "kitty" . into( ) ] ) ,
377+ is_deprecated: false ,
287378 } ,
288379 ParsedVariant {
289380 name: "Dog" . into( ) ,
290381 ty: Some ( "DogInfo" . into( ) ) ,
291382 description: None ,
292383 rename: Some ( "dog" . into( ) ) ,
293384 aliases: None ,
385+ is_deprecated: false ,
294386 } ,
295387 ] ,
296388 } ;
@@ -303,6 +395,44 @@ mod tests {
303395 assert ! ( code. contains( " Cat(CatInfo)," ) ) ;
304396 }
305397
398+ #[ test]
399+ fn test_generate_enum_primitive_variants ( ) {
400+ // Test untagged enum with primitives: [String, i32]
401+ let en = ParsedEnum {
402+ name : "IntOrString" . into ( ) ,
403+ description : None ,
404+ rename : None ,
405+ tag : None ,
406+ untagged : true ,
407+ is_deprecated : false ,
408+ external_docs : None ,
409+ variants : vec ! [
410+ ParsedVariant {
411+ name: "String" . into( ) ,
412+ ty: Some ( "String" . into( ) ) ,
413+ description: None ,
414+ rename: None ,
415+ aliases: None ,
416+ is_deprecated: false ,
417+ } ,
418+ ParsedVariant {
419+ name: "Integer" . into( ) ,
420+ ty: Some ( "i32" . into( ) ) ,
421+ description: None ,
422+ rename: None ,
423+ aliases: None ,
424+ is_deprecated: false ,
425+ } ,
426+ ] ,
427+ } ;
428+
429+ let code = generate_dtos ( & [ ParsedModel :: Enum ( en) ] ) ;
430+ assert ! ( code. contains( "pub enum IntOrString" ) ) ;
431+ assert ! ( code. contains( "#[serde(untagged)]" ) ) ;
432+ assert ! ( code. contains( " String(String)," ) ) ;
433+ assert ! ( code. contains( " Integer(i32)," ) ) ;
434+ }
435+
306436 #[ test]
307437 fn test_flattened_imports ( ) {
308438 // Simulating a struct that resulted from allOf flattening
@@ -311,6 +441,8 @@ mod tests {
311441 name : "Merged" . into ( ) ,
312442 description : None ,
313443 rename : None ,
444+ is_deprecated : false ,
445+ external_docs : None ,
314446 fields : vec ! [ field( "id" , "Uuid" ) , field( "meta" , "serde_json::Value" ) ] ,
315447 } ;
316448
@@ -321,4 +453,35 @@ mod tests {
321453 assert ! ( code. contains( "pub id: Uuid," ) ) ;
322454 assert ! ( code. contains( "pub meta: serde_json::Value," ) ) ;
323455 }
456+
457+ #[ test]
458+ fn test_additional_properties_handling ( ) {
459+ // Struct with HashMap field representing additional properties
460+ let dto = ParsedStruct {
461+ name : "Dict" . into ( ) ,
462+ description : None ,
463+ rename : None ,
464+ is_deprecated : false ,
465+ external_docs : None ,
466+ fields : vec ! [
467+ field( "static_field" , "String" ) ,
468+ ParsedField {
469+ name: "additional_properties" . into( ) ,
470+ ty: "std::collections::HashMap<String, i32>" . into( ) ,
471+ description: Some ( "Props" . into( ) ) ,
472+ rename: None ,
473+ is_skipped: false ,
474+ is_deprecated: false ,
475+ external_docs: None ,
476+ } ,
477+ ] ,
478+ } ;
479+
480+ let code = generate_dto ( & dto) ;
481+ // Should inject import
482+ assert ! ( code. contains( "use std::collections::HashMap;" ) ) ;
483+ // Should inject flatten attribute
484+ assert ! ( code. contains( "#[serde(flatten)]" ) ) ;
485+ assert ! ( code. contains( "pub additional_properties: std::collections::HashMap<String, i32>" ) ) ;
486+ }
324487}
0 commit comments