diff --git a/.changepacks/changepack_log_cgh9Sk7aKVb-x_Vv9J4Pv.json b/.changepacks/changepack_log_cgh9Sk7aKVb-x_Vv9J4Pv.json new file mode 100644 index 0000000..3eee3c6 --- /dev/null +++ b/.changepacks/changepack_log_cgh9Sk7aKVb-x_Vv9J4Pv.json @@ -0,0 +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"} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 788a722..5d69a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,7 +1672,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vespera" -version = "0.1.13" +version = "0.1.15" dependencies = [ "axum", "axum-extra", @@ -1682,7 +1682,7 @@ dependencies = [ [[package]] name = "vespera_core" -version = "0.1.13" +version = "0.1.15" dependencies = [ "rstest", "serde", @@ -1691,7 +1691,7 @@ dependencies = [ [[package]] name = "vespera_macro" -version = "0.1.13" +version = "0.1.15" dependencies = [ "anyhow", "insta", diff --git a/crates/vespera_macro/src/parser/schema.rs b/crates/vespera_macro/src/parser/schema.rs index f8ddda4..cf339fe 100644 --- a/crates/vespera_macro/src/parser/schema.rs +++ b/crates/vespera_macro/src/parser/schema.rs @@ -744,9 +744,20 @@ pub(super) fn parse_type_to_schema_ref_with_schemas( return SchemaRef::Inline(Box::new(Schema::array(inner_schema))); } else { // Option -> nullable schema - if let SchemaRef::Inline(mut schema) = inner_schema { - schema.nullable = Some(true); - return SchemaRef::Inline(schema); + match inner_schema { + SchemaRef::Inline(mut schema) => { + schema.nullable = Some(true); + return SchemaRef::Inline(schema); + } + SchemaRef::Ref(reference) => { + // Wrap reference in an inline schema to attach nullable flag + return SchemaRef::Inline(Box::new(Schema { + ref_path: Some(reference.ref_path), + schema_type: None, + nullable: Some(true), + ..Schema::new(SchemaType::Object) + })); + } } } } @@ -919,6 +930,27 @@ mod tests { } } + #[test] + fn test_parse_type_to_schema_ref_option_ref_nullable() { + let mut known = HashMap::new(); + known.insert("User".to_string(), "struct User;".to_string()); + + let ty: syn::Type = syn::parse_str("Option").unwrap(); + let schema_ref = parse_type_to_schema_ref(&ty, &known, &HashMap::new()); + + match schema_ref { + SchemaRef::Inline(schema) => { + assert_eq!( + schema.ref_path, + Some("#/components/schemas/User".to_string()) + ); + assert_eq!(schema.nullable, Some(true)); + assert_eq!(schema.schema_type, None); + } + _ => panic!("Expected inline schema for Option"), + } + } + #[rstest] #[case( r#" diff --git a/examples/axum-example/openapi.json b/examples/axum-example/openapi.json index 013e241..7a28cbc 100644 --- a/examples/axum-example/openapi.json +++ b/examples/axum-example/openapi.json @@ -1467,6 +1467,17 @@ "value2" ] }, + "InSkipResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, "MapQuery": { "type": "object", "properties": { @@ -1563,6 +1574,44 @@ "type": "string", "default": "default42" }, + "in_skip": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "in_skip2": { + "$ref": "#/components/schemas/InSkipResponse", + "nullable": true + }, + "in_skip3": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + } + }, + "in_skip4": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip5": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip6": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, "name": { "type": "string" }, @@ -1572,7 +1621,9 @@ } }, "required": [ - "name" + "name", + "in_skip", + "in_skip3" ] }, "StructBody": { diff --git a/examples/axum-example/src/routes/users.rs b/examples/axum-example/src/routes/users.rs index a4f4e33..661f876 100644 --- a/examples/axum-example/src/routes/users.rs +++ b/examples/axum-example/src/routes/users.rs @@ -1,3 +1,5 @@ +use std::collections::{BTreeMap, HashMap}; + use serde::{Deserialize, Serialize}; use vespera::{ Schema, @@ -84,6 +86,18 @@ pub struct SkipResponse { #[serde(rename = "num", default)] pub num: i32, + + pub in_skip: InSkipResponse, + pub in_skip2: Option, + pub in_skip3: Vec, + pub in_skip4: Option>, + pub in_skip5: Option>, + pub in_skip6: Option>, +} + +#[derive(Serialize, Deserialize, Schema)] +pub struct InSkipResponse { + pub name: String, } fn default_value() -> String { @@ -102,5 +116,23 @@ pub async fn skip_response() -> Json { email6: "john.doe6@example.com".to_string(), email7: "john.doe7@example.com".to_string(), num: 0, + in_skip: InSkipResponse { + name: "John Doe".to_string(), + }, + in_skip2: Some(InSkipResponse { + name: "John Doe".to_string(), + }), + in_skip3: vec![InSkipResponse { + name: "John Doe".to_string(), + }], + in_skip4: Some(vec![InSkipResponse { + name: "John Doe".to_string(), + }]), + in_skip5: Some(HashMap::from([("John Doe".to_string(), InSkipResponse { + name: "John Doe".to_string(), + })]),), + in_skip6: Some(BTreeMap::from([("John Doe".to_string(), InSkipResponse { + name: "John Doe".to_string(), + })]),), }) } diff --git a/examples/axum-example/tests/snapshots/integration_test__openapi.snap b/examples/axum-example/tests/snapshots/integration_test__openapi.snap index 2f50579..4435d9e 100644 --- a/examples/axum-example/tests/snapshots/integration_test__openapi.snap +++ b/examples/axum-example/tests/snapshots/integration_test__openapi.snap @@ -1471,6 +1471,17 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "value2" ] }, + "InSkipResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, "MapQuery": { "type": "object", "properties": { @@ -1567,6 +1578,44 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" "type": "string", "default": "default42" }, + "in_skip": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "in_skip2": { + "$ref": "#/components/schemas/InSkipResponse", + "nullable": true + }, + "in_skip3": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + } + }, + "in_skip4": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip5": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip6": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, "name": { "type": "string" }, @@ -1576,7 +1625,9 @@ expression: "std::fs::read_to_string(\"openapi.json\").unwrap()" } }, "required": [ - "name" + "name", + "in_skip", + "in_skip3" ] }, "StructBody": { diff --git a/openapi.json b/openapi.json index 013e241..7a28cbc 100644 --- a/openapi.json +++ b/openapi.json @@ -1467,6 +1467,17 @@ "value2" ] }, + "InSkipResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, "MapQuery": { "type": "object", "properties": { @@ -1563,6 +1574,44 @@ "type": "string", "default": "default42" }, + "in_skip": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "in_skip2": { + "$ref": "#/components/schemas/InSkipResponse", + "nullable": true + }, + "in_skip3": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + } + }, + "in_skip4": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip5": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, + "in_skip6": { + "type": "object", + "properties": {}, + "required": [], + "additionalProperties": { + "$ref": "#/components/schemas/InSkipResponse" + }, + "nullable": true + }, "name": { "type": "string" }, @@ -1572,7 +1621,9 @@ } }, "required": [ - "name" + "name", + "in_skip", + "in_skip3" ] }, "StructBody": {