Skip to content

Commit 84f21f7

Browse files
LeinnanMrGVSV
andauthored
Schema types metadata (#19524)
# Objective - Currently there is predefinied list of supported DataTypes that can be detected on Bevy JSON Schema generation and mapped as reflect_types array elements. - Make it possible to register custom `reflectTypes` mappings for Bevy JSON Schema. ## Solution - Create a `SchemaTypesMetadata` Resource that will hold mappings for `TypeId` of `TypeData`. List is bigger from beggining and it is possible to expand it without forking package. ## Testing - I use it for quite a while in my game, I have a fork of bevy_remote with more changes that later I want to merge to main as well. --------- Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
1 parent ffd6c9e commit 84f21f7

File tree

5 files changed

+217
-42
lines changed

5 files changed

+217
-42
lines changed

crates/bevy_remote/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ license = "MIT OR Apache-2.0"
99
keywords = ["bevy"]
1010

1111
[features]
12-
default = ["http"]
12+
default = ["http", "bevy_asset"]
1313
http = ["dep:async-io", "dep:smol-hyper"]
14+
bevy_asset = ["dep:bevy_asset"]
1415

1516
[dependencies]
1617
# bevy
@@ -28,6 +29,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea
2829
"std",
2930
"serialize",
3031
] }
32+
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true }
3133

3234
# other
3335
anyhow = "1"

crates/bevy_remote/src/builtin_methods.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use serde_json::{Map, Value};
2424

2525
use crate::{
2626
error_codes,
27-
schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument},
27+
schemas::{
28+
json_schema::{export_type, JsonSchemaBevyType},
29+
open_rpc::OpenRpcDocument,
30+
},
2831
BrpError, BrpResult,
2932
};
3033

@@ -1223,32 +1226,35 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
12231226
Some(params) => parse(params)?,
12241227
};
12251228

1229+
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>();
12261230
let types = world.resource::<AppTypeRegistry>();
12271231
let types = types.read();
12281232
let schemas = types
12291233
.iter()
1230-
.map(crate::schemas::json_schema::export_type)
1231-
.filter(|(_, schema)| {
1232-
if let Some(crate_name) = &schema.crate_name {
1234+
.filter_map(|type_reg| {
1235+
let path_table = type_reg.type_info().type_path_table();
1236+
if let Some(crate_name) = &path_table.crate_name() {
12331237
if !filter.with_crates.is_empty()
12341238
&& !filter.with_crates.iter().any(|c| crate_name.eq(c))
12351239
{
1236-
return false;
1240+
return None;
12371241
}
12381242
if !filter.without_crates.is_empty()
12391243
&& filter.without_crates.iter().any(|c| crate_name.eq(c))
12401244
{
1241-
return false;
1245+
return None;
12421246
}
12431247
}
1248+
let (id, schema) = export_type(type_reg, extra_info);
1249+
12441250
if !filter.type_limit.with.is_empty()
12451251
&& !filter
12461252
.type_limit
12471253
.with
12481254
.iter()
12491255
.any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc)))
12501256
{
1251-
return false;
1257+
return None;
12521258
}
12531259
if !filter.type_limit.without.is_empty()
12541260
&& filter
@@ -1257,10 +1263,9 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
12571263
.iter()
12581264
.any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc)))
12591265
{
1260-
return false;
1266+
return None;
12611267
}
1262-
1263-
true
1268+
Some((id.to_string(), schema))
12641269
})
12651270
.collect::<HashMap<String, JsonSchemaBevyType>>();
12661271

crates/bevy_remote/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@
364364
//! [fully-qualified type names]: bevy_reflect::TypePath::type_path
365365
//! [fully-qualified type name]: bevy_reflect::TypePath::type_path
366366
367+
extern crate alloc;
368+
367369
use async_channel::{Receiver, Sender};
368370
use bevy_app::{prelude::*, MainScheduleOrder};
369371
use bevy_derive::{Deref, DerefMut};
@@ -539,6 +541,7 @@ impl Plugin for RemotePlugin {
539541
.insert_after(Last, RemoteLast);
540542

541543
app.insert_resource(remote_methods)
544+
.init_resource::<schemas::SchemaTypesMetadata>()
542545
.init_resource::<RemoteWatchingRequests>()
543546
.add_systems(PreStartup, setup_mailbox_channel)
544547
.configure_sets(

crates/bevy_remote/src/schemas/json_schema.rs

Lines changed: 132 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,63 @@
11
//! Module with JSON Schema type for Bevy Registry Types.
22
//! It tries to follow this standard: <https://json-schema.org/specification>
3-
use bevy_ecs::reflect::{ReflectComponent, ReflectResource};
3+
use alloc::borrow::Cow;
44
use bevy_platform::collections::HashMap;
55
use bevy_reflect::{
6-
prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize,
7-
TypeInfo, TypeRegistration, VariantInfo,
6+
GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry,
7+
VariantInfo,
88
};
99
use core::any::TypeId;
1010
use serde::{Deserialize, Serialize};
1111
use serde_json::{json, Map, Value};
1212

13-
/// Exports schema info for a given type
14-
pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) {
15-
(reg.type_info().type_path().to_owned(), reg.into())
13+
use crate::schemas::SchemaTypesMetadata;
14+
15+
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
16+
pub trait TypeRegistrySchemaReader {
17+
/// Export type JSON Schema.
18+
fn export_type_json_schema<T: GetTypeRegistration + 'static>(
19+
&self,
20+
extra_info: &SchemaTypesMetadata,
21+
) -> Option<JsonSchemaBevyType> {
22+
self.export_type_json_schema_for_id(extra_info, TypeId::of::<T>())
23+
}
24+
/// Export type JSON Schema.
25+
fn export_type_json_schema_for_id(
26+
&self,
27+
extra_info: &SchemaTypesMetadata,
28+
type_id: TypeId,
29+
) -> Option<JsonSchemaBevyType>;
1630
}
1731

18-
fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec<String> {
19-
// Vec could be moved to allow registering more types by game maker.
20-
let registered_reflect_types: [(TypeId, &str); 5] = [
21-
{ (TypeId::of::<ReflectComponent>(), "Component") },
22-
{ (TypeId::of::<ReflectResource>(), "Resource") },
23-
{ (TypeId::of::<ReflectDefault>(), "Default") },
24-
{ (TypeId::of::<ReflectSerialize>(), "Serialize") },
25-
{ (TypeId::of::<ReflectDeserialize>(), "Deserialize") },
26-
];
27-
let mut result = Vec::new();
28-
for (id, name) in registered_reflect_types {
29-
if reg.data_by_id(id).is_some() {
30-
result.push(name.to_owned());
31-
}
32+
impl TypeRegistrySchemaReader for TypeRegistry {
33+
fn export_type_json_schema_for_id(
34+
&self,
35+
extra_info: &SchemaTypesMetadata,
36+
type_id: TypeId,
37+
) -> Option<JsonSchemaBevyType> {
38+
let type_reg = self.get(type_id)?;
39+
Some((type_reg, extra_info).into())
3240
}
33-
result
3441
}
3542

36-
impl From<&TypeRegistration> for JsonSchemaBevyType {
37-
fn from(reg: &TypeRegistration) -> Self {
43+
/// Exports schema info for a given type
44+
pub fn export_type(
45+
reg: &TypeRegistration,
46+
metadata: &SchemaTypesMetadata,
47+
) -> (Cow<'static, str>, JsonSchemaBevyType) {
48+
(reg.type_info().type_path().into(), (reg, metadata).into())
49+
}
50+
51+
impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
52+
fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {
53+
let (reg, metadata) = value;
3854
let t = reg.type_info();
3955
let binding = t.type_path_table();
4056

4157
let short_path = binding.short_path();
4258
let type_path = binding.path();
4359
let mut typed_schema = JsonSchemaBevyType {
44-
reflect_types: get_registered_reflect_types(reg),
60+
reflect_types: metadata.get_registered_reflect_types(reg),
4561
short_path: short_path.to_owned(),
4662
type_path: type_path.to_owned(),
4763
crate_name: binding.crate_name().map(str::to_owned),
@@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField {
351367
#[cfg(test)]
352368
mod tests {
353369
use super::*;
370+
use bevy_ecs::prelude::ReflectComponent;
371+
use bevy_ecs::prelude::ReflectResource;
372+
354373
use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};
355-
use bevy_reflect::Reflect;
374+
use bevy_reflect::prelude::ReflectDefault;
375+
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
356376

357377
#[test]
358378
fn reflect_export_struct() {
@@ -373,7 +393,7 @@ mod tests {
373393
.get(TypeId::of::<Foo>())
374394
.expect("SHOULD BE REGISTERED")
375395
.clone();
376-
let (_, schema) = export_type(&foo_registration);
396+
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
377397

378398
assert!(
379399
!schema.reflect_types.contains(&"Component".to_owned()),
@@ -418,7 +438,7 @@ mod tests {
418438
.get(TypeId::of::<EnumComponent>())
419439
.expect("SHOULD BE REGISTERED")
420440
.clone();
421-
let (_, schema) = export_type(&foo_registration);
441+
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
422442
assert!(
423443
schema.reflect_types.contains(&"Component".to_owned()),
424444
"Should be a component"
@@ -453,7 +473,7 @@ mod tests {
453473
.get(TypeId::of::<EnumComponent>())
454474
.expect("SHOULD BE REGISTERED")
455475
.clone();
456-
let (_, schema) = export_type(&foo_registration);
476+
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
457477
assert!(
458478
!schema.reflect_types.contains(&"Component".to_owned()),
459479
"Should not be a component"
@@ -466,6 +486,62 @@ mod tests {
466486
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
467487
}
468488

489+
#[test]
490+
fn reflect_struct_with_custom_type_data() {
491+
#[derive(Reflect, Default, Deserialize, Serialize)]
492+
#[reflect(Default)]
493+
enum EnumComponent {
494+
ValueOne(i32),
495+
ValueTwo {
496+
test: i32,
497+
},
498+
#[default]
499+
NoValue,
500+
}
501+
502+
#[derive(Clone)]
503+
pub struct ReflectCustomData;
504+
505+
impl<T: Reflect> bevy_reflect::FromType<T> for ReflectCustomData {
506+
fn from_type() -> Self {
507+
ReflectCustomData
508+
}
509+
}
510+
511+
let atr = AppTypeRegistry::default();
512+
{
513+
let mut register = atr.write();
514+
register.register::<EnumComponent>();
515+
register.register_type_data::<EnumComponent, ReflectCustomData>();
516+
}
517+
let mut metadata = SchemaTypesMetadata::default();
518+
metadata.map_type_data::<ReflectCustomData>("CustomData");
519+
let type_registry = atr.read();
520+
let foo_registration = type_registry
521+
.get(TypeId::of::<EnumComponent>())
522+
.expect("SHOULD BE REGISTERED")
523+
.clone();
524+
let (_, schema) = export_type(&foo_registration, &metadata);
525+
assert!(
526+
!metadata.has_type_data::<ReflectComponent>(&schema.reflect_types),
527+
"Should not be a component"
528+
);
529+
assert!(
530+
!metadata.has_type_data::<ReflectResource>(&schema.reflect_types),
531+
"Should not be a resource"
532+
);
533+
assert!(
534+
metadata.has_type_data::<ReflectDefault>(&schema.reflect_types),
535+
"Should have default"
536+
);
537+
assert!(
538+
metadata.has_type_data::<ReflectCustomData>(&schema.reflect_types),
539+
"Should have CustomData"
540+
);
541+
assert!(schema.properties.is_empty(), "Should not have any field");
542+
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
543+
}
544+
469545
#[test]
470546
fn reflect_export_tuple_struct() {
471547
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
@@ -482,7 +558,7 @@ mod tests {
482558
.get(TypeId::of::<TupleStructType>())
483559
.expect("SHOULD BE REGISTERED")
484560
.clone();
485-
let (_, schema) = export_type(&foo_registration);
561+
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
486562
assert!(
487563
schema.reflect_types.contains(&"Component".to_owned()),
488564
"Should be a component"
@@ -513,7 +589,7 @@ mod tests {
513589
.get(TypeId::of::<Foo>())
514590
.expect("SHOULD BE REGISTERED")
515591
.clone();
516-
let (_, schema) = export_type(&foo_registration);
592+
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
517593
let schema_as_value = serde_json::to_value(&schema).expect("Should serialize");
518594
let value = json!({
519595
"shortPath": "Foo",
@@ -538,6 +614,31 @@ mod tests {
538614
"a"
539615
]
540616
});
541-
assert_eq!(schema_as_value, value);
617+
assert_normalized_values(schema_as_value, value);
618+
}
619+
620+
/// This function exist to avoid false failures due to ordering differences between `serde_json` values.
621+
fn assert_normalized_values(mut one: Value, mut two: Value) {
622+
normalize_json(&mut one);
623+
normalize_json(&mut two);
624+
assert_eq!(one, two);
625+
626+
/// Recursively sorts arrays in a `serde_json::Value`
627+
fn normalize_json(value: &mut Value) {
628+
match value {
629+
Value::Array(arr) => {
630+
for v in arr.iter_mut() {
631+
normalize_json(v);
632+
}
633+
arr.sort_by_key(ToString::to_string); // Sort by stringified version
634+
}
635+
Value::Object(map) => {
636+
for (_k, v) in map.iter_mut() {
637+
normalize_json(v);
638+
}
639+
}
640+
_ => {}
641+
}
642+
}
542643
}
543644
}

0 commit comments

Comments
 (0)