diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 176e5ed47f..28e0fc6f2b 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -9,8 +9,9 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http"] +default = ["http", "bevy_asset"] http = ["dep:async-io", "dep:smol-hyper"] +bevy_asset = ["dep:bevy_asset"] [dependencies] # bevy @@ -28,6 +29,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea "std", "serialize", ] } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true } # other anyhow = "1" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index e390f448c4..0bf3f9773b 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -24,7 +24,10 @@ use serde_json::{Map, Value}; use crate::{ error_codes, - schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, + schemas::{ + json_schema::{export_type, JsonSchemaBevyType}, + open_rpc::OpenRpcDocument, + }, BrpError, BrpResult, }; @@ -1223,24 +1226,27 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br Some(params) => parse(params)?, }; + let extra_info = world.resource::(); let types = world.resource::(); let types = types.read(); let schemas = types .iter() - .map(crate::schemas::json_schema::export_type) - .filter(|(_, schema)| { - if let Some(crate_name) = &schema.crate_name { + .filter_map(|type_reg| { + let path_table = type_reg.type_info().type_path_table(); + if let Some(crate_name) = &path_table.crate_name() { if !filter.with_crates.is_empty() && !filter.with_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } if !filter.without_crates.is_empty() && filter.without_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } } + let (id, schema) = export_type(type_reg, extra_info); + if !filter.type_limit.with.is_empty() && !filter .type_limit @@ -1248,7 +1254,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } if !filter.type_limit.without.is_empty() && filter @@ -1257,10 +1263,9 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } - - true + Some((id.to_string(), schema)) }) .collect::>(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..348be8089d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -364,6 +364,8 @@ //! [fully-qualified type names]: bevy_reflect::TypePath::type_path //! [fully-qualified type name]: bevy_reflect::TypePath::type_path +extern crate alloc; + use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -539,6 +541,7 @@ impl Plugin for RemotePlugin { .insert_after(Last, RemoteLast); app.insert_resource(remote_methods) + .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 3fcc588f92..4e56625bc8 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -1,47 +1,63 @@ //! Module with JSON Schema type for Bevy Registry Types. //! It tries to follow this standard: -use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use alloc::borrow::Cow; use bevy_platform::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, VariantInfo, + GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -/// Exports schema info for a given type -pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - (reg.type_info().type_path().to_owned(), reg.into()) -} +use crate::schemas::SchemaTypesMetadata; -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } +/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` +pub trait TypeRegistrySchemaReader { + /// Export type JSON Schema. + fn export_type_json_schema( + &self, + extra_info: &SchemaTypesMetadata, + ) -> Option { + self.export_type_json_schema_for_id(extra_info, TypeId::of::()) } - result + /// Export type JSON Schema. + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option; } -impl From<&TypeRegistration> for JsonSchemaBevyType { - fn from(reg: &TypeRegistration) -> Self { +impl TypeRegistrySchemaReader for TypeRegistry { + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option { + let type_reg = self.get(type_id)?; + Some((type_reg, extra_info).into()) + } +} + +/// Exports schema info for a given type +pub fn export_type( + reg: &TypeRegistration, + metadata: &SchemaTypesMetadata, +) -> (Cow<'static, str>, JsonSchemaBevyType) { + (reg.type_info().type_path().into(), (reg, metadata).into()) +} + +impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { + fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { + let (reg, metadata) = value; let t = reg.type_info(); let binding = t.type_path_table(); let short_path = binding.short_path(); let type_path = binding.path(); let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), + reflect_types: metadata.get_registered_reflect_types(reg), short_path: short_path.to_owned(), type_path: type_path.to_owned(), crate_name: binding.crate_name().map(str::to_owned), @@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField { #[cfg(test)] mod tests { use super::*; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::prelude::ReflectResource; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; - use bevy_reflect::Reflect; + use bevy_reflect::prelude::ReflectDefault; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; #[test] fn reflect_export_struct() { @@ -373,7 +393,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -418,7 +438,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -453,7 +473,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), "Should not be a component" @@ -466,6 +486,62 @@ mod tests { assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); } + #[test] + fn reflect_struct_with_custom_type_data() { + #[derive(Reflect, Default, Deserialize, Serialize)] + #[reflect(Default)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + #[derive(Clone)] + pub struct ReflectCustomData; + + impl bevy_reflect::FromType for ReflectCustomData { + fn from_type() -> Self { + ReflectCustomData + } + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register_type_data::(); + } + let mut metadata = SchemaTypesMetadata::default(); + metadata.map_type_data::("CustomData"); + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration, &metadata); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a component" + ); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a resource" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have default" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have CustomData" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + #[test] fn reflect_export_tuple_struct() { #[derive(Reflect, Component, Default, Deserialize, Serialize)] @@ -482,7 +558,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -513,7 +589,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", @@ -538,6 +614,31 @@ mod tests { "a" ] }); - assert_eq!(schema_as_value, value); + assert_normalized_values(schema_as_value, value); + } + + /// This function exist to avoid false failures due to ordering differences between `serde_json` values. + fn assert_normalized_values(mut one: Value, mut two: Value) { + normalize_json(&mut one); + normalize_json(&mut two); + assert_eq!(one, two); + + /// Recursively sorts arrays in a `serde_json::Value` + fn normalize_json(value: &mut Value) { + match value { + Value::Array(arr) => { + for v in arr.iter_mut() { + normalize_json(v); + } + arr.sort_by_key(ToString::to_string); // Sort by stringified version + } + Value::Object(map) => { + for (_k, v) in map.iter_mut() { + normalize_json(v); + } + } + _ => {} + } + } } } diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 7104fd5547..10cb2e9421 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,4 +1,68 @@ //! Module with schemas used for various BRP endpoints +use bevy_ecs::{ + reflect::{ReflectComponent, ReflectResource}, + resource::Resource, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, + TypeRegistration, +}; +use core::any::TypeId; pub mod json_schema; pub mod open_rpc; + +/// Holds mapping of reflect [type data](TypeData) to strings, +/// later on used in Bevy Json Schema. +#[derive(Debug, Resource, Reflect)] +#[reflect(Resource)] +pub struct SchemaTypesMetadata { + /// Type Data id mapping to strings. + pub type_data_map: HashMap, +} + +impl Default for SchemaTypesMetadata { + fn default() -> Self { + let mut data_types = Self { + type_data_map: Default::default(), + }; + data_types.map_type_data::("Component"); + data_types.map_type_data::("Resource"); + data_types.map_type_data::("Default"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("Asset"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("AssetHandle"); + data_types.map_type_data::("Serialize"); + data_types.map_type_data::("Deserialize"); + data_types + } +} + +impl SchemaTypesMetadata { + /// Map `TypeId` of `TypeData` to string + pub fn map_type_data(&mut self, name: impl Into) { + self.type_data_map.insert(TypeId::of::(), name.into()); + } + + /// Build reflect types list for a given type registration + pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec { + self.type_data_map + .iter() + .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) + .collect() + } + + /// Checks if slice contains string value that matches checked `TypeData` + pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { + self.has_type_data_by_id(TypeId::of::(), types_string_slice) + } + + /// Checks if slice contains string value that matches checked `TypeData` by id. + pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { + self.type_data_map + .get(&id) + .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) + } +}