Skip to content

Commit 8a7ea33

Browse files
committed
Fix struct option issue
1 parent 8cf4c1c commit 8a7ea33

File tree

7 files changed

+227
-9
lines changed

7 files changed

+227
-9
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"crates/vespera_macro/Cargo.toml":"Patch","crates/vespera_core/Cargo.toml":"Patch","crates/vespera/Cargo.toml":"Patch"},"note":"Fix struct in option issue","date":"2025-12-12T07:26:22.869740900Z"}

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespera_macro/src/parser/schema.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,20 @@ pub(super) fn parse_type_to_schema_ref_with_schemas(
744744
return SchemaRef::Inline(Box::new(Schema::array(inner_schema)));
745745
} else {
746746
// Option<T> -> nullable schema
747-
if let SchemaRef::Inline(mut schema) = inner_schema {
748-
schema.nullable = Some(true);
749-
return SchemaRef::Inline(schema);
747+
match inner_schema {
748+
SchemaRef::Inline(mut schema) => {
749+
schema.nullable = Some(true);
750+
return SchemaRef::Inline(schema);
751+
}
752+
SchemaRef::Ref(reference) => {
753+
// Wrap reference in an inline schema to attach nullable flag
754+
return SchemaRef::Inline(Box::new(Schema {
755+
ref_path: Some(reference.ref_path),
756+
schema_type: None,
757+
nullable: Some(true),
758+
..Schema::new(SchemaType::Object)
759+
}));
760+
}
750761
}
751762
}
752763
}
@@ -919,6 +930,27 @@ mod tests {
919930
}
920931
}
921932

933+
#[test]
934+
fn test_parse_type_to_schema_ref_option_ref_nullable() {
935+
let mut known = HashMap::new();
936+
known.insert("User".to_string(), "struct User;".to_string());
937+
938+
let ty: syn::Type = syn::parse_str("Option<User>").unwrap();
939+
let schema_ref = parse_type_to_schema_ref(&ty, &known, &HashMap::new());
940+
941+
match schema_ref {
942+
SchemaRef::Inline(schema) => {
943+
assert_eq!(
944+
schema.ref_path,
945+
Some("#/components/schemas/User".to_string())
946+
);
947+
assert_eq!(schema.nullable, Some(true));
948+
assert_eq!(schema.schema_type, None);
949+
}
950+
_ => panic!("Expected inline schema for Option<User>"),
951+
}
952+
}
953+
922954
#[rstest]
923955
#[case(
924956
r#"

examples/axum-example/openapi.json

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,17 @@
14671467
"value2"
14681468
]
14691469
},
1470+
"InSkipResponse": {
1471+
"type": "object",
1472+
"properties": {
1473+
"name": {
1474+
"type": "string"
1475+
}
1476+
},
1477+
"required": [
1478+
"name"
1479+
]
1480+
},
14701481
"MapQuery": {
14711482
"type": "object",
14721483
"properties": {
@@ -1563,6 +1574,44 @@
15631574
"type": "string",
15641575
"default": "default42"
15651576
},
1577+
"in_skip": {
1578+
"$ref": "#/components/schemas/InSkipResponse"
1579+
},
1580+
"in_skip2": {
1581+
"$ref": "#/components/schemas/InSkipResponse",
1582+
"nullable": true
1583+
},
1584+
"in_skip3": {
1585+
"type": "array",
1586+
"items": {
1587+
"$ref": "#/components/schemas/InSkipResponse"
1588+
}
1589+
},
1590+
"in_skip4": {
1591+
"type": "array",
1592+
"items": {
1593+
"$ref": "#/components/schemas/InSkipResponse"
1594+
},
1595+
"nullable": true
1596+
},
1597+
"in_skip5": {
1598+
"type": "object",
1599+
"properties": {},
1600+
"required": [],
1601+
"additionalProperties": {
1602+
"$ref": "#/components/schemas/InSkipResponse"
1603+
},
1604+
"nullable": true
1605+
},
1606+
"in_skip6": {
1607+
"type": "object",
1608+
"properties": {},
1609+
"required": [],
1610+
"additionalProperties": {
1611+
"$ref": "#/components/schemas/InSkipResponse"
1612+
},
1613+
"nullable": true
1614+
},
15661615
"name": {
15671616
"type": "string"
15681617
},
@@ -1572,7 +1621,9 @@
15721621
}
15731622
},
15741623
"required": [
1575-
"name"
1624+
"name",
1625+
"in_skip",
1626+
"in_skip3"
15761627
]
15771628
},
15781629
"StructBody": {

examples/axum-example/src/routes/users.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2+
13
use serde::{Deserialize, Serialize};
24
use vespera::{
35
Schema,
@@ -84,6 +86,18 @@ pub struct SkipResponse {
8486

8587
#[serde(rename = "num", default)]
8688
pub num: i32,
89+
90+
pub in_skip: InSkipResponse,
91+
pub in_skip2: Option<InSkipResponse>,
92+
pub in_skip3: Vec<InSkipResponse>,
93+
pub in_skip4: Option<Vec<InSkipResponse>>,
94+
pub in_skip5: Option<HashMap<String, InSkipResponse>>,
95+
pub in_skip6: Option<BTreeMap<String, InSkipResponse>>,
96+
}
97+
98+
#[derive(Serialize, Deserialize, Schema)]
99+
pub struct InSkipResponse {
100+
pub name: String,
87101
}
88102

89103
fn default_value() -> String {
@@ -102,5 +116,23 @@ pub async fn skip_response() -> Json<SkipResponse> {
102116
email6: "[email protected]".to_string(),
103117
email7: "[email protected]".to_string(),
104118
num: 0,
119+
in_skip: InSkipResponse {
120+
name: "John Doe".to_string(),
121+
},
122+
in_skip2: Some(InSkipResponse {
123+
name: "John Doe".to_string(),
124+
}),
125+
in_skip3: vec![InSkipResponse {
126+
name: "John Doe".to_string(),
127+
}],
128+
in_skip4: Some(vec![InSkipResponse {
129+
name: "John Doe".to_string(),
130+
}]),
131+
in_skip5: Some(HashMap::from([("John Doe".to_string(), InSkipResponse {
132+
name: "John Doe".to_string(),
133+
})]),),
134+
in_skip6: Some(BTreeMap::from([("John Doe".to_string(), InSkipResponse {
135+
name: "John Doe".to_string(),
136+
})]),),
105137
})
106138
}

examples/axum-example/tests/snapshots/integration_test__openapi.snap

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,17 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()"
14711471
"value2"
14721472
]
14731473
},
1474+
"InSkipResponse": {
1475+
"type": "object",
1476+
"properties": {
1477+
"name": {
1478+
"type": "string"
1479+
}
1480+
},
1481+
"required": [
1482+
"name"
1483+
]
1484+
},
14741485
"MapQuery": {
14751486
"type": "object",
14761487
"properties": {
@@ -1567,6 +1578,44 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()"
15671578
"type": "string",
15681579
"default": "default42"
15691580
},
1581+
"in_skip": {
1582+
"$ref": "#/components/schemas/InSkipResponse"
1583+
},
1584+
"in_skip2": {
1585+
"$ref": "#/components/schemas/InSkipResponse",
1586+
"nullable": true
1587+
},
1588+
"in_skip3": {
1589+
"type": "array",
1590+
"items": {
1591+
"$ref": "#/components/schemas/InSkipResponse"
1592+
}
1593+
},
1594+
"in_skip4": {
1595+
"type": "array",
1596+
"items": {
1597+
"$ref": "#/components/schemas/InSkipResponse"
1598+
},
1599+
"nullable": true
1600+
},
1601+
"in_skip5": {
1602+
"type": "object",
1603+
"properties": {},
1604+
"required": [],
1605+
"additionalProperties": {
1606+
"$ref": "#/components/schemas/InSkipResponse"
1607+
},
1608+
"nullable": true
1609+
},
1610+
"in_skip6": {
1611+
"type": "object",
1612+
"properties": {},
1613+
"required": [],
1614+
"additionalProperties": {
1615+
"$ref": "#/components/schemas/InSkipResponse"
1616+
},
1617+
"nullable": true
1618+
},
15701619
"name": {
15711620
"type": "string"
15721621
},
@@ -1576,7 +1625,9 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()"
15761625
}
15771626
},
15781627
"required": [
1579-
"name"
1628+
"name",
1629+
"in_skip",
1630+
"in_skip3"
15801631
]
15811632
},
15821633
"StructBody": {

openapi.json

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,17 @@
14671467
"value2"
14681468
]
14691469
},
1470+
"InSkipResponse": {
1471+
"type": "object",
1472+
"properties": {
1473+
"name": {
1474+
"type": "string"
1475+
}
1476+
},
1477+
"required": [
1478+
"name"
1479+
]
1480+
},
14701481
"MapQuery": {
14711482
"type": "object",
14721483
"properties": {
@@ -1563,6 +1574,44 @@
15631574
"type": "string",
15641575
"default": "default42"
15651576
},
1577+
"in_skip": {
1578+
"$ref": "#/components/schemas/InSkipResponse"
1579+
},
1580+
"in_skip2": {
1581+
"$ref": "#/components/schemas/InSkipResponse",
1582+
"nullable": true
1583+
},
1584+
"in_skip3": {
1585+
"type": "array",
1586+
"items": {
1587+
"$ref": "#/components/schemas/InSkipResponse"
1588+
}
1589+
},
1590+
"in_skip4": {
1591+
"type": "array",
1592+
"items": {
1593+
"$ref": "#/components/schemas/InSkipResponse"
1594+
},
1595+
"nullable": true
1596+
},
1597+
"in_skip5": {
1598+
"type": "object",
1599+
"properties": {},
1600+
"required": [],
1601+
"additionalProperties": {
1602+
"$ref": "#/components/schemas/InSkipResponse"
1603+
},
1604+
"nullable": true
1605+
},
1606+
"in_skip6": {
1607+
"type": "object",
1608+
"properties": {},
1609+
"required": [],
1610+
"additionalProperties": {
1611+
"$ref": "#/components/schemas/InSkipResponse"
1612+
},
1613+
"nullable": true
1614+
},
15661615
"name": {
15671616
"type": "string"
15681617
},
@@ -1572,7 +1621,9 @@
15721621
}
15731622
},
15741623
"required": [
1575-
"name"
1624+
"name",
1625+
"in_skip",
1626+
"in_skip3"
15761627
]
15771628
},
15781629
"StructBody": {

0 commit comments

Comments
 (0)