Skip to content

Commit 3ccc572

Browse files
committed
Improve OpenAPI 3.2.0 compliance
1 parent e863824 commit 3ccc572

32 files changed

+1923
-1024
lines changed

core/src/codegen.rs

Lines changed: 180 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
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
1313
use crate::error::{AppError, AppResult};
14+
use crate::parser::models::ParsedExternalDocs;
1415
use crate::parser::{ParsedEnum, ParsedModel, ParsedStruct};
1516
use ra_ap_edition::Edition;
1617
use 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).
108132
fn 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 {
158197
fn 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)]
243293
mod 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

Comments
 (0)