diff --git a/src/openapi.rs b/src/openapi.rs index 368fb32..32b05b6 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -154,6 +154,16 @@ pub enum Schema { #[serde(flatten)] details: SchemaDetails, }, + /// AllOf composition (must come before Typed to handle type + allOf + /// patterns) + AllOf { + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + schema_type: Option, + #[serde(rename = "allOf")] + all_of: Vec, + #[serde(flatten)] + details: SchemaDetails, + }, /// Schema with `type` as an array (OpenAPI 3.1 / JSON Schema 2020-12). /// The canonical 3.1 way to express a nullable type is /// `type: ["string", "null"]`. Listed before `Typed` so the array form @@ -171,13 +181,6 @@ pub enum Schema { #[serde(flatten)] details: SchemaDetails, }, - /// AllOf composition - AllOf { - #[serde(rename = "allOf")] - all_of: Vec, - #[serde(flatten)] - details: SchemaDetails, - }, /// Schema without explicit type (inferred from other fields) Untyped { #[serde(flatten)] diff --git a/src/snapshots/openapi_to_rust__test_helpers__allof_type_object_sibling_test.snap b/src/snapshots/openapi_to_rust__test_helpers__allof_type_object_sibling_test.snap new file mode 100644 index 0000000..882a74e --- /dev/null +++ b/src/snapshots/openapi_to_rust__test_helpers__allof_type_object_sibling_test.snap @@ -0,0 +1,31 @@ +--- +source: src/test_helpers.rs +assertion_line: 229 +expression: "&generated_code" +--- +//! Generated types from OpenAPI specification +//! +//! This file contains all the generated types for the API. +//! Do not edit manually - regenerate using the appropriate script. +#![allow(clippy::large_enum_variant)] +#![allow(clippy::format_in_format_args)] +#![allow(clippy::let_unit_value)] +#![allow(unreachable_patterns)] +use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct Widget { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct WidgetExtra { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct WidgetBase { + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} diff --git a/tests/serde_json_value_reduction_tests.rs b/tests/serde_json_value_reduction_tests.rs index 6f1d2bb..3b66ade 100644 --- a/tests/serde_json_value_reduction_tests.rs +++ b/tests/serde_json_value_reduction_tests.rs @@ -307,6 +307,48 @@ fn test_nested_allof_composition() { assert!(result.contains("pub content: String")); } +#[test] +fn test_allof_with_redundant_type_object_sibling() { + // Test AllOf composition that should flatten properties instead of using serde_json::Value + // even with type: object. + let spec = json!({ + "openapi": "3.1.0", + "info": {"title": "Test", "version": "1.0"}, + "components": { + "schemas": { + "Widget": { + "type": "object", + "allOf": [ + {"$ref": "#/components/schemas/WidgetBase"}, + {"$ref": "#/components/schemas/WidgetExtra"} + ] + }, + "WidgetBase": { + "type": "object", + "properties": { + "id": {"type": "string"} + } + }, + "WidgetExtra": { + "type": "object", + "properties": { + "name": {"type": "string"} + } + } + } + } + }); + + let result = + test_generation("allof_type_object_sibling_test", spec).expect("Generation failed"); + + assert!(result.contains("pub struct Widget")); + // Check different possible formats of string. + assert!(result.contains("pub id: Option") || result.contains("pub id: String")); + assert!(result.contains("pub name: Option") || result.contains("pub name: String")); + assert!(!result.contains("pub type Widget = serde_json::Value")); +} + #[test] fn test_object_with_additional_properties() { // Test that objects with additionalProperties correctly use BTreeMap diff --git a/tests/server_raw_body_roundtrip_test.rs b/tests/server_raw_body_roundtrip_test.rs index 6d797ab..9c0a9d8 100644 --- a/tests/server_raw_body_roundtrip_test.rs +++ b/tests/server_raw_body_roundtrip_test.rs @@ -401,6 +401,21 @@ mod tests { } } + fn sample_plugin() -> Plugin { + Plugin { + added_at: "2024-01-01T00:00:00Z".parse().unwrap(), + connection: PluginModeUnion::PluginSupervisedProps(PluginSupervisedProps {}), + description: None, + id: "plugin-one".to_string(), + manifest: PluginManifest::default(), + name: "Test Plugin".to_string(), + status: PluginStatus::PluginStatusActive(PluginStatusActive { + activated_at: "2024-01-01T00:00:00Z".parse().unwrap(), + }), + version: None, + } + } + #[async_trait::async_trait] impl PluginsApi for Api { async fn plugin_update_package( @@ -412,7 +427,7 @@ mod tests { self.captured .send(("plugin".into(), body.map(|value| value.to_vec()))) .unwrap(); - PluginUpdatePackageResponse::Ok(serde_json::json!({"updated": true})) + PluginUpdatePackageResponse::Ok(sample_plugin()) } } @@ -437,7 +452,10 @@ mod tests { .plugin_update_package("plugin-one", Some(archive.clone())) .await .unwrap(); - assert_eq!(response, serde_json::json!({"updated": true})); + assert_eq!( + serde_json::to_value(&response).unwrap(), + serde_json::to_value(sample_plugin()).unwrap() + ); assert_eq!( captured_rx.recv().await.unwrap(), ("plugin".into(), Some(archive))