Schema Types Metadata

This commit is contained in:
Piotr Siuszko 2025-06-07 12:34:04 +02:00
parent de79d3f363
commit 2f082b63e9
4 changed files with 148 additions and 40 deletions

View File

@ -26,6 +26,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea
"std", "std",
"serialize", "serialize",
] } ] }
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
# other # other
anyhow = "1" anyhow = "1"

View File

@ -24,7 +24,7 @@ use serde_json::{Map, Value};
use crate::{ use crate::{
error_codes, error_codes,
schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, schemas::{json_schema::{export_type, JsonSchemaBevyType}, open_rpc::OpenRpcDocument},
BrpError, BrpResult, BrpError, BrpResult,
}; };
@ -1223,13 +1223,25 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
Some(params) => parse(params)?, Some(params) => parse(params)?,
}; };
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>();
let types = world.resource::<AppTypeRegistry>(); let types = world.resource::<AppTypeRegistry>();
let types = types.read(); let types = types.read();
let schemas = types let schemas = types
.iter() .iter()
.map(crate::schemas::json_schema::export_type) .filter(|type_reg| {
.filter(|(_, schema)| { // EXTRA FILTER, not decided yet if gonna make it in the end
if let Some(crate_name) = &schema.crate_name { match type_reg.type_info() {
bevy_reflect::TypeInfo::Tuple(_) => return false,
bevy_reflect::TypeInfo::TupleStruct(_) => return false,
bevy_reflect::TypeInfo::List(_) => return false,
bevy_reflect::TypeInfo::Array(_) => return false,
bevy_reflect::TypeInfo::Map(_) => return false,
bevy_reflect::TypeInfo::Set(_) => return false,
_ => {}
}
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() if !filter.with_crates.is_empty()
&& !filter.with_crates.iter().any(|c| crate_name.eq(c)) && !filter.with_crates.iter().any(|c| crate_name.eq(c))
{ {
@ -1241,6 +1253,12 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
return false; return false;
} }
} }
true
})
.flat_map(|e| {
let (id, schema) = export_type(e, extra_info);
if !filter.type_limit.with.is_empty() if !filter.type_limit.with.is_empty()
&& !filter && !filter
.type_limit .type_limit
@ -1248,7 +1266,7 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
.iter() .iter()
.any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc)))
{ {
return false; return None;
} }
if !filter.type_limit.without.is_empty() if !filter.type_limit.without.is_empty()
&& filter && filter
@ -1257,10 +1275,9 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
.iter() .iter()
.any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc)))
{ {
return false; return None;
} }
Some((id, schema))
true
}) })
.collect::<HashMap<String, JsonSchemaBevyType>>(); .collect::<HashMap<String, JsonSchemaBevyType>>();

View File

@ -1,47 +1,61 @@
//! Module with JSON Schema type for Bevy Registry Types. //! Module with JSON Schema type for Bevy Registry Types.
//! It tries to follow this standard: <https://json-schema.org/specification> //! It tries to follow this standard: <https://json-schema.org/specification>
use bevy_ecs::reflect::{ReflectComponent, ReflectResource};
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_reflect::{ use bevy_reflect::{
prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, VariantInfo
TypeInfo, TypeRegistration, VariantInfo,
}; };
use core::any::TypeId; use core::any::TypeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value}; use serde_json::{json, Map, Value};
/// Exports schema info for a given type use crate::schemas::SchemaTypesMetadata;
pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) {
(reg.type_info().type_path().to_owned(), reg.into())
}
fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec<String> {
// Vec could be moved to allow registering more types by game maker. /// Helper trait for converting TypeRegistration to JsonSchemaBevyType
let registered_reflect_types: [(TypeId, &str); 5] = [ pub trait TypeRegistrySchemaReader {
{ (TypeId::of::<ReflectComponent>(), "Component") }, /// Export type JSON Schema with definitions.
{ (TypeId::of::<ReflectResource>(), "Resource") }, /// It can be useful for generating schemas for assets validation.
{ (TypeId::of::<ReflectDefault>(), "Default") }, fn export_type_json_schema<T: GetTypeRegistration>(
{ (TypeId::of::<ReflectSerialize>(), "Serialize") }, &self,
{ (TypeId::of::<ReflectDeserialize>(), "Deserialize") }, extra_info: &SchemaTypesMetadata,
]; ) -> Option<JsonSchemaBevyType> {
let mut result = Vec::new(); self.export_type_json_schema_for_id(extra_info, T::get_type_registration().type_id())
for (id, name) in registered_reflect_types {
if reg.data_by_id(id).is_some() {
result.push(name.to_owned());
}
} }
result /// Export type JSON Schema with definitions.
/// It can be useful for generating schemas for assets validation.
fn export_type_json_schema_for_id(
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
) -> Option<JsonSchemaBevyType>;
} }
impl From<&TypeRegistration> for JsonSchemaBevyType { impl TypeRegistrySchemaReader for TypeRegistry {
fn from(reg: &TypeRegistration) -> Self { fn export_type_json_schema_for_id(
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
) -> Option<JsonSchemaBevyType> {
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) -> (String, JsonSchemaBevyType) {
(reg.type_info().type_path().to_owned(), (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 t = reg.type_info();
let binding = t.type_path_table(); let binding = t.type_path_table();
let short_path = binding.short_path(); let short_path = binding.short_path();
let type_path = binding.path(); let type_path = binding.path();
let mut typed_schema = JsonSchemaBevyType { 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(), short_path: short_path.to_owned(),
type_path: type_path.to_owned(), type_path: type_path.to_owned(),
crate_name: binding.crate_name().map(str::to_owned), crate_name: binding.crate_name().map(str::to_owned),
@ -351,8 +365,11 @@ impl SchemaJsonReference for &NamedField {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use bevy_ecs::prelude::ReflectResource;
use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};
use bevy_reflect::Reflect; use bevy_reflect::prelude::ReflectDefault;
use bevy_ecs::prelude::ReflectComponent;
use bevy_reflect::{Reflect,ReflectSerialize, ReflectDeserialize};
#[test] #[test]
fn reflect_export_struct() { fn reflect_export_struct() {
@ -373,7 +390,7 @@ mod tests {
.get(TypeId::of::<Foo>()) .get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED") .expect("SHOULD BE REGISTERED")
.clone(); .clone();
let (_, schema) = export_type(&foo_registration); let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
assert!( assert!(
!schema.reflect_types.contains(&"Component".to_owned()), !schema.reflect_types.contains(&"Component".to_owned()),
@ -418,7 +435,7 @@ mod tests {
.get(TypeId::of::<EnumComponent>()) .get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED") .expect("SHOULD BE REGISTERED")
.clone(); .clone();
let (_, schema) = export_type(&foo_registration); let (_, schema) = export_type(&foo_registration,&SchemaTypesMetadata::default());
assert!( assert!(
schema.reflect_types.contains(&"Component".to_owned()), schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component" "Should be a component"
@ -453,7 +470,7 @@ mod tests {
.get(TypeId::of::<EnumComponent>()) .get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED") .expect("SHOULD BE REGISTERED")
.clone(); .clone();
let (_, schema) = export_type(&foo_registration); let (_, schema) = export_type(&foo_registration,&SchemaTypesMetadata::default());
assert!( assert!(
!schema.reflect_types.contains(&"Component".to_owned()), !schema.reflect_types.contains(&"Component".to_owned()),
"Should not be a component" "Should not be a component"
@ -482,7 +499,7 @@ mod tests {
.get(TypeId::of::<TupleStructType>()) .get(TypeId::of::<TupleStructType>())
.expect("SHOULD BE REGISTERED") .expect("SHOULD BE REGISTERED")
.clone(); .clone();
let (_, schema) = export_type(&foo_registration); let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
assert!( assert!(
schema.reflect_types.contains(&"Component".to_owned()), schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component" "Should be a component"
@ -513,7 +530,7 @@ mod tests {
.get(TypeId::of::<Foo>()) .get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED") .expect("SHOULD BE REGISTERED")
.clone(); .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 schema_as_value = serde_json::to_value(&schema).expect("Should serialize");
let value = json!({ let value = json!({
"shortPath": "Foo", "shortPath": "Foo",
@ -538,6 +555,32 @@ mod tests {
"a" "a"
] ]
}); });
assert_eq!(schema_as_value, value); assert_normalized_values(schema_as_value, value);
}
fn assert_normalized_values(one: Value, two: Value){
let mut one = one.clone();
normalize_json(&mut one);
let mut two = two.clone();
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(|a, b| a.to_string().cmp(&b.to_string())); // Sort by stringified version
}
Value::Object(map) => {
for (_k, v) in map.iter_mut() {
normalize_json(v);
}
}
_ => {}
}
}
} }
} }

View File

@ -1,4 +1,51 @@
//! Module with schemas used for various BRP endpoints //! Module with schemas used for various BRP endpoints
use std::any::TypeId;
use bevy_asset::{ReflectAsset, ReflectHandle};
use bevy_ecs::{reflect::{ReflectComponent, ReflectResource}, resource::Resource};
use bevy_platform::collections::HashMap;
use bevy_reflect::{prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, TypeRegistration};
pub mod json_schema; pub mod json_schema;
pub mod open_rpc; pub mod open_rpc;
/// Holds mapping of reflect data types to strings,
/// later on used in Bevy Json Schema.
#[derive(Debug, Resource, Reflect)]
#[reflect(Resource)]
pub struct SchemaTypesMetadata {
/// data types id mapping to strings.
pub data_types: HashMap<TypeId, String>,
}
impl Default for SchemaTypesMetadata {
fn default() -> Self {
let mut data_types = Self {
data_types: Default::default(),
};
data_types.register_type::<ReflectComponent>("Component");
data_types.register_type::<ReflectResource>("Resource");
data_types.register_type::<ReflectDefault>("Default");
data_types.register_type::<ReflectAsset>("Asset");
data_types.register_type::<ReflectHandle>("AssetHandle");
data_types.register_type::<ReflectSerialize>("Serialize");
data_types.register_type::<ReflectDeserialize>("Deserialize");
data_types
}
}
impl SchemaTypesMetadata {
/// Map TypeId of TypeData to string
pub fn register_type<T: TypeData>(&mut self, name: impl Into<String>) {
self.data_types.insert(TypeId::of::<T>(), name.into());
}
/// build reflect types list for a given type registration
pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec<String> {
self.data_types
.iter()
.flat_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone())))
.collect()
}
}