From f5cb1c78b08dabb50d7e764e68f7f518594207b3 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Tue, 8 Jul 2025 17:45:37 +0200 Subject: [PATCH] export schema now stores possible values to serialize in top 'one_of' field, support for Unit like structs serialization" --- crates/bevy_remote/src/builtin_methods.rs | 149 ++++++++++++++++-- .../bevy_remote/src/schemas/reflect_info.rs | 15 +- 2 files changed, 149 insertions(+), 15 deletions(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 1f31276b29..2ae7282f24 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -27,9 +27,9 @@ use serde_json::{Map, Value}; use crate::{ error_codes, schemas::{ - json_schema::{JsonSchemaBevyType, SchemaMarker}, + json_schema::{JsonSchemaBevyType, JsonSchemaVariant, SchemaMarker}, open_rpc::OpenRpcDocument, - reflect_info::TypeDefinitionBuilder, + reflect_info::{TypeDefinitionBuilder, TypeReferencePath}, }, BrpError, BrpResult, }; @@ -1437,6 +1437,35 @@ fn export_registry_types_typed( Some((schema_id, Box::new(schema))) }) .collect(); + schema.one_of = schema + .definitions + .iter() + .flat_map(|(id, schema)| { + if !schema.reflect_type_data.contains(&"Serialize".into()) + || !schema.reflect_type_data.contains(&"Deserialize".into()) + { + return None; + } + let schema = JsonSchemaBevyType { + properties: [( + schema.type_path.clone(), + JsonSchemaBevyType { + ref_type: Some(TypeReferencePath::definition(id.clone())), + description: schema.description.clone(), + ..Default::default() + } + .into(), + )] + .into(), + schema_type: Some(crate::schemas::json_schema::SchemaType::Object.into()), + additional_properties: Some(JsonSchemaVariant::BoolValue(false)), + description: schema.description.clone(), + reflect_type_data: schema.reflect_type_data.clone(), + ..Default::default() + }; + Some(schema.into()) + }) + .collect(); Ok(schema) } @@ -1680,9 +1709,9 @@ mod tests { } use bevy_ecs::component::Component; - use bevy_reflect::Reflect; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; - use crate::schemas::{reflect_info::TypeReferenceId, SchemaTypesMetadata}; + use crate::schemas::SchemaTypesMetadata; use super::*; @@ -1709,23 +1738,113 @@ mod tests { } #[test] - #[cfg(feature = "bevy_math")] - fn export_schema_test() { - #[derive(Reflect, Default, Deserialize, Serialize)] + fn reflection_serialization_tests() { + use bevy_ecs::resource::Resource; + + #[derive(Reflect, Default, Deserialize, Serialize, Resource)] + #[reflect(Resource, Serialize, Deserialize)] + pub struct ResourceStruct { + /// FIELD DOC + pub field: String, + pub second_field: Option<(u8, Option)>, + } + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component, Serialize, Deserialize)] pub struct OtherStruct { /// FIELD DOC pub field: String, - pub second_field: Option<(u8, u8)>, + pub second_field: Option<(u8, Option)>, } /// STRUCT DOC - #[derive(Reflect, Default, Deserialize, Serialize)] - pub struct SecondStruct { - pub field: String, - /// FIELD DOC + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component, Serialize, Deserialize)] + pub struct SecondStruct; + /// STRUCT DOC + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component, Serialize, Deserialize)] + pub struct ThirdStruct { + pub array_strings: Vec, + pub array_structs: [OtherStruct; 5], + // pub map_strings: HashMap, + } + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component, Serialize, Deserialize)] + pub struct NestedStruct { pub other: OtherStruct, + pub second: SecondStruct, + /// DOC FOR FIELD + pub third: ThirdStruct, + } + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register::(); + } + let value = NestedStruct { + other: OtherStruct { + field: "S".into(), + second_field: Some((0, None)), + }, + second: SecondStruct, + third: ThirdStruct { + array_strings: ["s".into(), "SS".into()].into(), + array_structs: [ + OtherStruct::default(), + OtherStruct::default(), + OtherStruct::default(), + OtherStruct::default(), + OtherStruct::default(), + ], + }, + }; + let mut world = World::new(); + world.insert_resource(atr.clone()); + world.insert_resource(SchemaTypesMetadata::default()); + let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world); + eprintln!( + "{}", + serde_json::to_string_pretty(&response).unwrap_or_default() + ); + let schema_value = serde_json::to_value(response).expect("Failed to serialize schema"); + let type_registry = atr.read(); + let serializer = ReflectSerializer::new(&value, &type_registry); + let res = ResourceStruct { + field: "SET".into(), + second_field: Some((45, None)), + }; + let serializer_2 = ReflectSerializer::new(&res, &type_registry); + let json = serde_json::to_value(&serializer).expect("msg"); + let validator = jsonschema::options() + .with_draft(jsonschema::Draft::Draft202012) + .build(&schema_value) + .expect("Failed to validate json schema"); + assert!( + validator.validate(&json).is_ok(), + "Validation failed: {}", + serde_json::to_string_pretty(&serializer).expect("msg") + ); + let json = serde_json::to_value(&serializer_2).expect("msg"); + assert!(validator.validate(&json).is_ok()); + } + + #[test] + #[cfg(feature = "bevy_math")] + fn export_schema_test() { + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component)] + pub struct OtherStruct { + /// FIELD DOC + pub field: String, + pub second_field: Option<(u8, Option)>, } /// STRUCT DOC - #[derive(Reflect, Default, Deserialize, Serialize)] + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component)] + pub struct SecondStruct; + /// STRUCT DOC + #[derive(Reflect, Default, Deserialize, Serialize, Component)] + #[reflect(Component)] pub struct ThirdStruct { pub array_strings: Vec, pub array_structs: [OtherStruct; 5], @@ -1740,7 +1859,6 @@ mod tests { pub third: ThirdStruct, } - let mut world = World::new(); let atr = AppTypeRegistry::default(); { use crate::schemas::ReflectJsonSchemaForceAsArray; @@ -1750,6 +1868,7 @@ mod tests { register.register::(); register.register_type_data::(); } + let mut world = World::new(); world.insert_resource(atr); world.insert_resource(SchemaTypesMetadata::default()); let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world); @@ -1780,6 +1899,8 @@ mod tests { response.definitions.keys() ); { + use crate::schemas::reflect_info::TypeReferenceId; + let first = response.definitions.iter().next().expect("Should have one"); assert_eq!(first.0, &TypeReferenceId::from("glam::Vec3")); } diff --git a/crates/bevy_remote/src/schemas/reflect_info.rs b/crates/bevy_remote/src/schemas/reflect_info.rs index b0520d4930..23e695bfc1 100644 --- a/crates/bevy_remote/src/schemas/reflect_info.rs +++ b/crates/bevy_remote/src/schemas/reflect_info.rs @@ -1043,7 +1043,17 @@ impl From<&InternalSchemaType> for Option { .. } => Some(SchemaTypeVariant::Single(*primitive)), InternalSchemaType::Array { .. } => Some(SchemaTypeVariant::Single(SchemaType::Array)), - InternalSchemaType::FieldsHolder(fields) => match fields.fields_type { + InternalSchemaType::FieldsHolder(fields) => match &fields.fields_type { + s if fields.fields.is_empty() => { + let first = if s.eq(&FieldType::Named) { + SchemaType::Object + } else { + SchemaType::Array + }; + Some(SchemaTypeVariant::Multiple( + [first, SchemaType::Null].into(), + )) + } FieldType::Named => Some(SchemaTypeVariant::Single(SchemaType::Object)), FieldType::Unnamed if fields.fields.len() == 1 => { let schema: SchemaType = fields.fields[0].type_id.into(); @@ -1270,6 +1280,9 @@ pub(crate) trait TypeDefinitionBuilder { schema: &mut JsonSchemaBevyType, info: &FieldsInformation, ) { + if info.fields.is_empty() { + return; + } match &info.fields_type { FieldType::Named => { schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));