Reworked building type dependencies list

This commit is contained in:
Piotr Siuszko 2025-07-06 11:30:34 +02:00
parent 0ff3201c3c
commit 64807b0d38
4 changed files with 604 additions and 124 deletions

View File

@ -27,9 +27,9 @@ use serde_json::{Map, Value};
use crate::{ use crate::{
error_codes, error_codes,
schemas::{ schemas::{
json_schema::{JsonSchemaBevyType, SchemaMarker, TypeRegistrySchemaReader}, json_schema::{JsonSchemaBevyType, SchemaMarker},
open_rpc::OpenRpcDocument, open_rpc::OpenRpcDocument,
reflect_info::TypeInformation, reflect_info::TypeDefinitionBuilder,
}, },
BrpError, BrpResult, BrpError, BrpResult,
}; };
@ -367,9 +367,11 @@ pub struct BrpJsonSchemaQueryFilter {
impl BrpJsonSchemaQueryFilter { impl BrpJsonSchemaQueryFilter {
/// Check if the filter should skip a type registration based on the crate name. /// Check if the filter should skip a type registration based on the crate name.
pub fn should_skip_for_crate(&self, type_registration: &TypeRegistration) -> bool { pub fn should_skip_for_crate(&self, type_registration: &TypeRegistration) -> bool {
let Some(crate_name) = type_registration.type_info().type_path_table().crate_name() else { let crate_name = type_registration
return false; .type_info()
}; .type_path_table()
.crate_name()
.unwrap_or_default();
if !self.with_crates.is_empty() && !self.with_crates.iter().any(|c| crate_name.eq(c)) { if !self.with_crates.is_empty() && !self.with_crates.iter().any(|c| crate_name.eq(c)) {
return true; return true;
} }
@ -1393,7 +1395,7 @@ fn export_registry_types_typed(
filter: BrpJsonSchemaQueryFilter, filter: BrpJsonSchemaQueryFilter,
world: &World, world: &World,
) -> Result<JsonSchemaBevyType, BrpError> { ) -> Result<JsonSchemaBevyType, BrpError> {
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>(); let metadata = world.resource::<crate::schemas::SchemaTypesMetadata>();
let types = world.resource::<AppTypeRegistry>(); let types = world.resource::<AppTypeRegistry>();
let types = types.read(); let types = types.read();
let mut schema = JsonSchemaBevyType { let mut schema = JsonSchemaBevyType {
@ -1403,14 +1405,14 @@ fn export_registry_types_typed(
), ),
..Default::default() ..Default::default()
}; };
schema.definitions = types let definitions: Vec<TypeId> = types
.iter() .iter()
.filter_map(|type_reg| { .filter_map(|type_reg| {
if filter.should_skip_for_crate(type_reg) { if filter.should_skip_for_crate(type_reg) {
return None; return None;
} }
if !filter.type_limit.is_empty() { if !filter.type_limit.is_empty() {
let registered_types = extra_info.get_registered_reflect_types(type_reg); let registered_types = metadata.get_registered_reflect_types(type_reg);
if filter if filter
.type_limit .type_limit
.should_skip_type(registered_types.as_slice()) .should_skip_type(registered_types.as_slice())
@ -1418,23 +1420,23 @@ fn export_registry_types_typed(
return None; return None;
} }
} }
let id = type_reg.type_id(); let type_id = type_reg.type_id();
let schema_type_info = TypeInformation::TypeRegistration(type_reg.clone()) let mut dep_ids = types.get_type_dependencies(type_id);
.to_schema_type_info_with_metadata(extra_info); dep_ids.insert(type_id);
let schema_id = schema_type_info.ty_info.try_get_type_reference_id()?; Some(dep_ids)
let mut definition = schema_type_info.to_definition();
definition.schema.default_value = types.try_get_default_value_for_type_id(id);
definition.schema.schema = None;
let mut definitions = vec![(schema_id, Box::new(definition.schema))];
let extra_defs = definition.definitions.iter().map(|(id, definition)| {
let def = definition.to_definition();
(id.clone(), Box::new(def.schema))
});
definitions.extend(extra_defs);
Some(definitions)
}) })
.flatten() .flatten()
.collect(); .collect();
schema.definitions = definitions
.into_iter()
.flat_map(|id| {
let result = types.build_schema_for_type_id(id, metadata);
let Some((Some(schema_id), schema)) = result else {
return None;
};
Some((schema_id, Box::new(schema)))
})
.collect();
Ok(schema) Ok(schema)
} }
@ -1678,12 +1680,11 @@ mod tests {
} }
use bevy_ecs::component::Component; use bevy_ecs::component::Component;
#[cfg(feature = "bevy_math")]
use bevy_math::Vec3; use bevy_math::Vec3;
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use crate::schemas::{ use crate::schemas::{reflect_info::TypeReferenceId, SchemaTypesMetadata};
reflect_info::TypeReferenceId, ReflectJsonSchemaForceAsArray, SchemaTypesMetadata,
};
use super::*; use super::*;
@ -1710,11 +1711,13 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "bevy_math")]
fn export_schema_test() { fn export_schema_test() {
#[derive(Reflect, Default, Deserialize, Serialize)] #[derive(Reflect, Default, Deserialize, Serialize)]
pub struct OtherStruct { pub struct OtherStruct {
/// FIELD DOC /// FIELD DOC
pub field: String, pub field: String,
pub second_field: Option<(u8, u8)>,
} }
/// STRUCT DOC /// STRUCT DOC
#[derive(Reflect, Default, Deserialize, Serialize)] #[derive(Reflect, Default, Deserialize, Serialize)]
@ -1742,6 +1745,8 @@ mod tests {
let mut world = World::new(); let mut world = World::new();
let atr = AppTypeRegistry::default(); let atr = AppTypeRegistry::default();
{ {
use crate::schemas::ReflectJsonSchemaForceAsArray;
let mut register = atr.write(); let mut register = atr.write();
register.register::<NestedStruct>(); register.register::<NestedStruct>();
register.register::<Vec3>(); register.register::<Vec3>();
@ -1750,21 +1755,32 @@ mod tests {
world.insert_resource(atr); world.insert_resource(atr);
world.insert_resource(SchemaTypesMetadata::default()); world.insert_resource(SchemaTypesMetadata::default());
let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world); let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world);
let schema_value = serde_json::to_value(&response).unwrap();
_ = jsonschema::options()
.with_draft(jsonschema::Draft::Draft202012)
.build(&schema_value)
.expect("Failed to validate json schema");
assert_eq!( assert_eq!(
response.definitions.len(), response.definitions.len(),
5, 6,
"Expected 5 definitions, got: {:#?}", "Expected 6 definitions, got: {}: {}",
response.definitions.keys() response.definitions.keys().len(),
serde_json::to_string_pretty(&response).unwrap_or_default()
); );
let response = export_registry_types_ext( let response = export_registry_types_ext(
BrpJsonSchemaQueryFilter { BrpJsonSchemaQueryFilter {
without_crates: vec!["bevy_remote".to_string()], with_crates: vec!["glam".to_string()],
..Default::default() ..Default::default()
}, },
&world, &world,
); );
assert_eq!(response.definitions.len(), 1); assert_eq!(
response.definitions.len(),
1,
"Expected 1 definition, got: {}: {:?}",
response.definitions.keys().len(),
response.definitions.keys()
);
{ {
let first = response.definitions.iter().next().expect("Should have one"); let first = response.definitions.iter().next().expect("Should have one");
assert_eq!(first.0, &TypeReferenceId::from("glam-Vec3")); assert_eq!(first.0, &TypeReferenceId::from("glam-Vec3"));
@ -1776,7 +1792,13 @@ mod tests {
}, },
&world, &world,
); );
assert_eq!(response.definitions.len(), 4); assert_eq!(
response.definitions.len(),
5,
"Expected 5 definitions, got: {}: {:?}",
response.definitions.len(),
response.definitions.keys()
);
let response = export_registry_types_ext( let response = export_registry_types_ext(
BrpJsonSchemaQueryFilter { BrpJsonSchemaQueryFilter {
type_limit: JsonSchemaTypeLimit { type_limit: JsonSchemaTypeLimit {
@ -1787,7 +1809,13 @@ mod tests {
}, },
&world, &world,
); );
assert_eq!(response.definitions.len(), 4); assert_eq!(
response.definitions.len(),
5,
"Expected 5 definitions, got: {}: {:?}",
response.definitions.len(),
response.definitions.keys()
);
} }
fn export_registry_types_ext( fn export_registry_types_ext(

View File

@ -47,17 +47,15 @@ impl TypeRegistrySchemaReader for TypeRegistry {
let mut definition = TypeInformation::from(type_reg) let mut definition = TypeInformation::from(type_reg)
.to_schema_type_info_with_metadata(extra_info) .to_schema_type_info_with_metadata(extra_info)
.to_definition(); .to_definition();
for missing in &definition.missing_definitions { for dependency in &definition.dependencies {
let reg_option = self.get(*missing); let reg_option = self.get(*dependency);
if let Some(reg) = reg_option { if let Some(reg) = reg_option {
let missing_schema = let missing_schema =
TypeInformation::from(reg).to_schema_type_info_with_metadata(extra_info); TypeInformation::from(reg).to_schema_type_info_with_metadata(extra_info);
let mis_def = missing_schema.to_definition(); let mis_def = missing_schema.to_definition();
definition.definitions.extend(mis_def.definitions); definition.definitions.extend(mis_def.definitions);
if let Some(missing_id) = mis_def.id { if let Some(missing_id) = mis_def.id {
if !definition.definitions.contains_key(&missing_id) { definition.definitions.insert(missing_id, missing_schema);
definition.definitions.insert(missing_id, missing_schema);
}
} }
} }
} }

View File

@ -43,7 +43,7 @@ pub(crate) trait RegisterReflectJsonSchemas {
fn register_schema_base_types(&mut self) { fn register_schema_base_types(&mut self) {
#[cfg(feature = "bevy_math")] #[cfg(feature = "bevy_math")]
{ {
// self.register_type_data_internal::<bevy_math::Vec2, ReflectJsonSchemaForceAsArray>(); self.register_type_data_internal::<bevy_math::Vec2, ReflectJsonSchemaForceAsArray>();
self.register_type_data_internal::<bevy_math::Vec3, ReflectJsonSchemaForceAsArray>(); self.register_type_data_internal::<bevy_math::Vec3, ReflectJsonSchemaForceAsArray>();
} }
self.register_type_internal::<OpenRpcDocument>(); self.register_type_internal::<OpenRpcDocument>();
@ -65,7 +65,9 @@ impl RegisterReflectJsonSchemas for bevy_reflect::TypeRegistry {
T: Reflect + bevy_reflect::TypePath, T: Reflect + bevy_reflect::TypePath,
D: TypeData + FromType<T>, D: TypeData + FromType<T>,
{ {
self.register_type_data::<T, D>(); if self.contains(TypeId::of::<T>()) {
self.register_type_data::<T, D>();
}
} }
fn register_type_internal<T>(&mut self) fn register_type_internal<T>(&mut self)

View File

@ -2,11 +2,11 @@
use alloc::borrow::Cow; use alloc::borrow::Cow;
use alloc::sync::Arc; use alloc::sync::Arc;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_platform::collections::HashMap; use bevy_platform::collections::{HashMap, HashSet};
use bevy_reflect::attributes::CustomAttributes; use bevy_reflect::attributes::CustomAttributes;
use bevy_reflect::{ use bevy_reflect::{
EnumInfo, GenericInfo, NamedField, Reflect, Type, TypeInfo, TypePathTable, TypeRegistration, EnumInfo, GenericInfo, NamedField, Reflect, Type, TypeInfo, TypePathTable, TypeRegistration,
UnnamedField, VariantInfo, TypeRegistry, UnnamedField, VariantInfo,
}; };
use bevy_utils::{default, TypeIdMap}; use bevy_utils::{default, TypeIdMap};
use core::any::TypeId; use core::any::TypeId;
@ -79,7 +79,7 @@ impl From<&str> for TypeReferenceId {
/// Information about the attributes of a field. /// Information about the attributes of a field.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct FieldInformation { pub(crate) struct FieldInformation {
/// Field specific data /// Field specific data
field: SchemaFieldData, field: SchemaFieldData,
/// Type information of the field. /// Type information of the field.
@ -88,7 +88,7 @@ pub struct FieldInformation {
/// Information about the field type. /// Information about the field type.
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Default)] #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Default)]
pub enum FieldType { pub(crate) enum FieldType {
/// Named field type. /// Named field type.
Named, Named,
/// Unnamed field type. /// Unnamed field type.
@ -100,7 +100,7 @@ pub enum FieldType {
/// Information about the attributes of a field. /// Information about the attributes of a field.
#[derive(Clone, Debug, Deref, DerefMut, Default)] #[derive(Clone, Debug, Deref, DerefMut, Default)]
pub struct FieldsInformation { pub(crate) struct FieldsInformation {
/// Fields information. /// Fields information.
#[deref] #[deref]
fields: Vec<FieldInformation>, fields: Vec<FieldInformation>,
@ -144,7 +144,7 @@ impl From<&TypeInformation> for Option<FieldsInformation> {
#[derive(Clone, Debug, Deref)] #[derive(Clone, Debug, Deref)]
/// Information about the attributes of a field. /// Information about the attributes of a field.
pub struct AttributesInformation(Arc<TypeIdMap<Box<dyn Reflect>>>); pub(crate) struct AttributesInformation(Arc<TypeIdMap<Box<dyn Reflect>>>);
impl From<&CustomAttributes> for AttributesInformation { impl From<&CustomAttributes> for AttributesInformation {
fn from(attributes: &CustomAttributes) -> Self { fn from(attributes: &CustomAttributes) -> Self {
@ -173,7 +173,7 @@ impl AttributeInfoReflect for CustomAttributes {
/// sources in the Bevy reflection system, allowing for flexible handling of type /// sources in the Bevy reflection system, allowing for flexible handling of type
/// metadata during schema generation. /// metadata during schema generation.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum TypeInformation { pub(crate) enum TypeInformation {
/// Contains a complete type registration with all associated metadata. /// Contains a complete type registration with all associated metadata.
/// ///
/// This variant holds a full `TypeRegistration` which includes type info, /// This variant holds a full `TypeRegistration` which includes type info,
@ -206,6 +206,14 @@ pub enum TypeInformation {
} }
impl TypeInformation { impl TypeInformation {
/// Find the type registration in the registry.
pub fn find_in_registry<'a>(&self, registry: &'a TypeRegistry) -> Option<&'a TypeRegistration> {
match self.try_get_type_path_table() {
Some(path_table) => registry.get_with_type_path(path_table.path()),
None => registry.get(self.type_id()),
}
}
/// Try to get a regex pattern for the type. /// Try to get a regex pattern for the type.
pub fn try_get_regex_for_type(&self) -> Option<Cow<'static, str>> { pub fn try_get_regex_for_type(&self) -> Option<Cow<'static, str>> {
let primitive_type = self.try_get_primitive_type()?; let primitive_type = self.try_get_primitive_type()?;
@ -315,16 +323,6 @@ impl TypeInformation {
} }
} }
/// Returns the type of the type.
pub fn try_get_type(&self) -> Option<&Type> {
match self {
TypeInformation::TypeInfo(type_info) => Some(type_info.ty()),
TypeInformation::TypeRegistration(reg) => Some(reg.type_info().ty()),
TypeInformation::Type(t) => Some(&**t),
_ => None,
}
}
/// Returns whether the type is forced as an array. /// Returns whether the type is forced as an array.
pub fn is_forced_as_array(&self) -> bool { pub fn is_forced_as_array(&self) -> bool {
match self { match self {
@ -365,7 +363,9 @@ impl TypeInformation {
type_info.type_id() type_info.type_id()
} }
} }
TypeInformation::TypeRegistration(type_registration) => type_registration.type_id(), TypeInformation::TypeRegistration(type_registration) => {
type_registration.type_info().type_id()
}
TypeInformation::VariantInfo(variant_info) => match &**variant_info { TypeInformation::VariantInfo(variant_info) => match &**variant_info {
VariantInfo::Struct(struct_info) => struct_info.type_id(), VariantInfo::Struct(struct_info) => struct_info.type_id(),
VariantInfo::Tuple(tuple_variant_info) => { VariantInfo::Tuple(tuple_variant_info) => {
@ -454,7 +454,7 @@ impl TryFrom<&TypeInformation> for TypeReferenceId {
/// Represents the data of a field in a schema. /// Represents the data of a field in a schema.
#[derive(Clone)] #[derive(Clone)]
pub struct SchemaFieldData { pub(crate) struct SchemaFieldData {
/// Name of the field. /// Name of the field.
pub name: Option<Cow<'static, str>>, pub name: Option<Cow<'static, str>>,
/// Index of the field. Can be provided for named fields when the data is obtained from containing struct definition. /// Index of the field. Can be provided for named fields when the data is obtained from containing struct definition.
@ -483,10 +483,6 @@ impl SchemaFieldData {
None => Cow::Owned(format!("[{}]", self.index.unwrap_or(0))), None => Cow::Owned(format!("[{}]", self.index.unwrap_or(0))),
} }
} }
/// Returns the index of the field.
pub fn index(&self) -> usize {
self.index.unwrap_or(0)
}
} }
/// Stores information about the location and id of a reference in a JSON schema. /// Stores information about the location and id of a reference in a JSON schema.
@ -889,7 +885,7 @@ pub(super) fn is_non_zero_number_type(t: TypeId) -> bool {
/// Enum representing the internal schema type information for different Rust types. /// Enum representing the internal schema type information for different Rust types.
/// This enum categorizes how different types should be represented in JSON schema. /// This enum categorizes how different types should be represented in JSON schema.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub enum InternalSchemaType { pub(crate) enum InternalSchemaType {
/// Represents array-like types (Vec, arrays, lists, sets). /// Represents array-like types (Vec, arrays, lists, sets).
Array { Array {
/// Element type information for the array. /// Element type information for the array.
@ -927,6 +923,116 @@ pub enum InternalSchemaType {
#[default] #[default]
NoInfo, NoInfo,
} }
impl InternalSchemaType {
/// Returns the dependencies of the type.
pub(super) fn get_dependencies(&self, registry: &TypeRegistry) -> HashSet<TypeId> {
let mut dependencies = HashSet::new();
match &self {
InternalSchemaType::Array {
element_ty,
min_size: _,
max_size: _,
} => {
if let Some(reg) = element_ty.find_in_registry(registry) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(reg.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
}
InternalSchemaType::EnumHolder(variant_infos) => {
for variant_info in variant_infos {
let sub_schema = InternalSchemaType::EnumVariant(variant_info.clone());
dependencies.extend(sub_schema.get_dependencies(registry));
}
}
InternalSchemaType::EnumVariant(variant_info) => match variant_info {
VariantInfo::Struct(struct_variant_info) => {
for field in struct_variant_info.iter() {
if let Some(reg) = registry.get(field.type_id()) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
}
}
VariantInfo::Tuple(tuple_variant_info) => {
for field in tuple_variant_info.iter() {
if let Some(reg) = registry.get(field.type_id()) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
}
}
VariantInfo::Unit(_) => {}
},
InternalSchemaType::FieldsHolder(fields_information) => {
for field in fields_information.iter() {
let Some(reg) = field.type_info.find_in_registry(registry) else {
continue;
};
if SchemaType::try_get_primitive_type_from_type_id(reg.type_id()).is_some() {
continue;
}
let info = TypeInformation::from(reg);
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
InternalSchemaType::Optional {
generic,
schema_type_info: _,
} => {
if let Some(reg) = registry.get(generic.type_id()) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
}
InternalSchemaType::Map { key, value } => {
if let Some(reg) = registry.get(key.type_id()) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
if let Some(reg) = registry.get(value.type_id()) {
let info = TypeInformation::from(reg);
if !info.is_primitive_type() {
let subschema: InternalSchemaType = (&info).into();
dependencies.insert(info.type_id());
dependencies.extend(subschema.get_dependencies(registry));
}
}
}
InternalSchemaType::RegularType(ty) => {
_ = dependencies.insert(ty.type_id());
}
InternalSchemaType::Regular(t) => {
_ = dependencies.insert(*t);
}
InternalSchemaType::NoInfo => {}
}
dependencies
}
}
impl From<&TypeInformation> for InternalSchemaType { impl From<&TypeInformation> for InternalSchemaType {
fn from(value: &TypeInformation) -> Self { fn from(value: &TypeInformation) -> Self {
let field_information: Option<FieldsInformation> = value.into(); let field_information: Option<FieldsInformation> = value.into();
@ -1064,7 +1170,7 @@ impl From<&FieldInformation> for SchemaTypeInfo {
/// This struct aggregates all the necessary information to generate a JSON schema /// This struct aggregates all the necessary information to generate a JSON schema
/// from Rust type information obtained through reflection. /// from Rust type information obtained through reflection.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SchemaDefinition { pub(crate) struct SchemaDefinition {
/// The type reference ID of the schema. /// The type reference ID of the schema.
pub id: Option<TypeReferenceId>, pub id: Option<TypeReferenceId>,
/// The JSON schema type of the schema. /// The JSON schema type of the schema.
@ -1073,7 +1179,7 @@ pub struct SchemaDefinition {
pub definitions: HashMap<TypeReferenceId, SchemaTypeInfo>, pub definitions: HashMap<TypeReferenceId, SchemaTypeInfo>,
/// Missing definitions of the schema. /// Missing definitions of the schema.
/// Could be the case for the types that are stored as generic arguments. /// Could be the case for the types that are stored as generic arguments.
pub missing_definitions: Vec<TypeId>, pub dependencies: Vec<TypeId>,
} }
impl From<SchemaDefinition> for JsonSchemaBevyType { impl From<SchemaDefinition> for JsonSchemaBevyType {
@ -1093,7 +1199,7 @@ impl From<SchemaDefinition> for JsonSchemaBevyType {
/// This struct aggregates all the necessary information to generate a JSON schema /// This struct aggregates all the necessary information to generate a JSON schema
/// from Rust type information obtained through reflection. /// from Rust type information obtained through reflection.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SchemaTypeInfo { pub(crate) struct SchemaTypeInfo {
/// Information about the type of the schema. /// Information about the type of the schema.
pub ty_info: TypeInformation, pub ty_info: TypeInformation,
/// Field information for the type. /// Field information for the type.
@ -1146,7 +1252,7 @@ impl SchemaTypeInfo {
.map(TypeReferencePath::definition); .map(TypeReferencePath::definition);
// If there is reference specified it is not need for specifying type // If there is reference specified it is not need for specifying type
let schema_type = if ref_type.is_some() { let schema_type = if ref_type.is_none() {
self.into() self.into()
} else { } else {
None None
@ -1240,15 +1346,15 @@ impl SchemaTypeInfo {
pub fn to_definition(&self) -> SchemaDefinition { pub fn to_definition(&self) -> SchemaDefinition {
let mut id: Option<TypeReferenceId> = self.ty_info.try_get_type_reference_id(); let mut id: Option<TypeReferenceId> = self.ty_info.try_get_type_reference_id();
let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new(); let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new();
let mut missing_definitions: Vec<TypeId> = Vec::new();
if let Some(custom_schema) = &self.ty_info.try_get_custom_schema() { if let Some(custom_schema) = &self.ty_info.try_get_custom_schema() {
return SchemaDefinition { return SchemaDefinition {
id, id,
schema: custom_schema.0.clone(), schema: custom_schema.0.clone(),
definitions, definitions,
missing_definitions, dependencies: Default::default(),
}; };
} }
let mut dependencies: HashSet<TypeId> = HashSet::new();
let range = self.get_range(); let range = self.get_range();
let (type_path, short_path, crate_name, module_path) = let (type_path, short_path, crate_name, module_path) =
@ -1303,9 +1409,9 @@ impl SchemaTypeInfo {
id, id,
schema: _, schema: _,
definitions: field_definitions, definitions: field_definitions,
missing_definitions: key_missing_definitions, dependencies: key_dependencies,
} = key.to_definition(); } = key.to_definition();
missing_definitions.extend(key_missing_definitions); dependencies.extend(key_dependencies);
if let Some(id) = id { if let Some(id) = id {
definitions.insert(id, key.clone()); definitions.insert(id, key.clone());
definitions.extend(field_definitions); definitions.extend(field_definitions);
@ -1317,10 +1423,10 @@ impl SchemaTypeInfo {
id, id,
schema: _, schema: _,
definitions: field_definitions, definitions: field_definitions,
missing_definitions: value_missing_definitions, dependencies: value_dependencies,
} = value.to_definition(); } = value.to_definition();
missing_definitions.extend(value_missing_definitions); dependencies.extend(value_dependencies);
if let Some(id) = id { if let Some(id) = id {
definitions.insert(id, value.clone()); definitions.insert(id, value.clone());
definitions.extend(field_definitions); definitions.extend(field_definitions);
@ -1415,7 +1521,7 @@ impl SchemaTypeInfo {
..Default::default() ..Default::default()
}, },
definitions: HashMap::new(), definitions: HashMap::new(),
missing_definitions, dependencies: dependencies.iter().cloned().collect(),
}; };
} }
} }
@ -1437,18 +1543,19 @@ impl SchemaTypeInfo {
if field_schema.ty_info.is_primitive_type() { if field_schema.ty_info.is_primitive_type() {
continue; continue;
} }
let SchemaDefinition { dependencies.insert(field_schema.ty_info.type_id());
id, // let SchemaDefinition {
schema: _, // id,
definitions: field_definitions, // schema: _,
missing_definitions: field_missing_definitions, // definitions: field_definitions,
} = field_schema.to_definition(); // missing_definitions: field_missing_definitions,
missing_definitions.extend(field_missing_definitions); // } = field_schema.to_definition();
definitions.extend(field_definitions); // missing_definitions.extend(field_missing_definitions);
let Some(id) = id else { continue }; // definitions.extend(field_definitions);
if !definitions.contains_key(&id) { // let Some(id) = id else { continue };
definitions.insert(id, field_schema); // if !definitions.contains_key(&id) {
} // definitions.insert(id, field_schema);
// }
} }
schema.required = fields schema.required = fields
.fields .fields
@ -1463,9 +1570,9 @@ impl SchemaTypeInfo {
id, id,
schema: new_schema_type, schema: new_schema_type,
definitions: field_definitions, definitions: field_definitions,
missing_definitions: field_missing_definitions, dependencies: field_dependencies,
} = field_schema.to_definition(); } = field_schema.to_definition();
missing_definitions.extend(field_missing_definitions); dependencies.extend(field_dependencies);
definitions.extend(field_definitions); definitions.extend(field_definitions);
if let Some(id) = id { if let Some(id) = id {
definitions.insert(id.clone(), field_schema); definitions.insert(id.clone(), field_schema);
@ -1534,10 +1641,10 @@ impl SchemaTypeInfo {
id, id,
schema: _, schema: _,
definitions: field_definitions, definitions: field_definitions,
missing_definitions: field_missing_definitions, dependencies: field_dependencies,
} = field_schema.to_definition(); } = field_schema.to_definition();
definitions.extend(field_definitions); definitions.extend(field_definitions);
missing_definitions.extend(field_missing_definitions); dependencies.extend(field_dependencies);
let Some(id) = id else { continue }; let Some(id) = id else { continue };
if !definitions.contains_key(&id) { if !definitions.contains_key(&id) {
definitions.insert(id, field_schema); definitions.insert(id, field_schema);
@ -1563,9 +1670,9 @@ impl SchemaTypeInfo {
id, id,
schema: _, schema: _,
definitions: field_definitions, definitions: field_definitions,
missing_definitions: field_missing_definitions, dependencies: field_dependencies,
} = items_schema.to_definition(); } = items_schema.to_definition();
missing_definitions.extend(field_missing_definitions); dependencies.extend(field_dependencies);
definitions.extend(field_definitions); definitions.extend(field_definitions);
if let Some(id) = id { if let Some(id) = id {
definitions.insert(id, items_schema); definitions.insert(id, items_schema);
@ -1580,9 +1687,9 @@ impl SchemaTypeInfo {
ty_info: TypeInformation::Type(Box::new(*generic.ty())), ty_info: TypeInformation::Type(Box::new(*generic.ty())),
..(**schema_type_info).clone() ..(**schema_type_info).clone()
}; };
missing_definitions.push(generic.type_id()); if SchemaType::try_get_primitive_type_from_type_id(generic.type_id()).is_none() {
let definition = schema_optional.clone().to_definition(); dependencies.insert(generic.type_id());
definitions.extend(definition.definitions); }
schema.ref_type = None; schema.ref_type = None;
schema.schema_type = None; schema.schema_type = None;
schema.one_of = vec![ schema.one_of = vec![
@ -1598,7 +1705,7 @@ impl SchemaTypeInfo {
id, id,
schema, schema,
definitions, definitions,
missing_definitions, dependencies: dependencies.into_iter().collect(),
} }
} }
} }
@ -1707,16 +1814,6 @@ where
} }
} }
/// Builds a JSON schema variant from a value.
pub fn build_schema<'a, T>(value: &'a T) -> JsonSchemaVariant
where
T: 'static,
SchemaTypeInfo: From<&'a T>,
{
let schema: SchemaTypeInfo = value.into();
schema.to_definition().schema.into()
}
impl From<&UnnamedField> for SchemaFieldData { impl From<&UnnamedField> for SchemaFieldData {
fn from(value: &UnnamedField) -> Self { fn from(value: &UnnamedField) -> Self {
let attributes: AttributesInformation = value.custom_attributes().into(); let attributes: AttributesInformation = value.custom_attributes().into();
@ -1832,6 +1929,334 @@ where
.collect() .collect()
} }
pub(crate) trait TypeDefinitionBuilder {
/// Builds a JSON schema for a given type ID.
fn build_schema_for_type_id(
&self,
type_id: TypeId,
metadata: &SchemaTypesMetadata,
) -> Option<(Option<TypeReferenceId>, JsonSchemaBevyType)>;
/// Returns a set of type IDs that are dependencies of the given type ID.
fn get_type_dependencies(&self, type_id: TypeId) -> HashSet<TypeId>;
/// Builds a JSON schema for a given type ID with definitions.
fn build_schema_for_type_id_with_definitions(
&self,
type_id: TypeId,
metadata: &SchemaTypesMetadata,
) -> Option<JsonSchemaBevyType>;
}
impl TypeDefinitionBuilder for TypeRegistry {
fn build_schema_for_type_id(
&self,
type_id: TypeId,
metadata: &SchemaTypesMetadata,
) -> Option<(Option<TypeReferenceId>, JsonSchemaBevyType)> {
let type_reg = self.get(type_id)?;
let type_info: TypeInformation = type_reg.into();
let schema_info = type_info.to_schema_type_info_with_metadata(metadata);
let mut id: Option<TypeReferenceId> = schema_info.ty_info.try_get_type_reference_id();
let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new();
if let Some(custom_schema) = &schema_info.ty_info.try_get_custom_schema() {
return Some((id, custom_schema.0.clone()));
}
let range = schema_info.get_range();
let (type_path, short_path, crate_name, module_path) =
if let Some(type_path_table) = schema_info.ty_info.try_get_type_path_table() {
(
type_path_table.path().into(),
type_path_table.short_path().into(),
type_path_table.crate_name().map(Into::into),
type_path_table.module_path().map(Into::into),
)
} else {
(Cow::default(), Cow::default(), None, None)
};
let not = if is_non_zero_number_type(schema_info.ty_info.type_id()) {
Some(Box::new(JsonSchemaBevyType {
const_value: Some(0.into()),
..default()
}))
} else {
None
};
let mut schema = JsonSchemaBevyType {
description: schema_info.ty_info.get_docs(),
not,
type_path,
short_path,
crate_name,
module_path,
kind: Some((&schema_info.ty_info).into()),
minimum: range.min.get_inclusive(),
maximum: range.max.get_inclusive(),
exclusive_minimum: range.min.get_exclusive(),
exclusive_maximum: range.max.get_exclusive(),
schema_type: (&schema_info).into(),
reflect_type_data: schema_info.reflect_type_data.clone().unwrap_or_default(),
..default()
};
match schema_info.internal_schema_type.clone() {
InternalSchemaType::Map { key, value } => {
let key = key.to_schema_type_info();
let value = value.to_schema_type_info();
if key.ty_info.try_get_primitive_type().is_some() {
if let Some(p) = key.ty_info.try_get_regex_for_type() {
schema.pattern_properties = [(p, value.to_ref_schema().into())].into();
schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));
}
}
schema.value_type = Some(value.to_ref_schema().into());
schema.key_type = Some(key.to_ref_schema().into());
}
InternalSchemaType::Regular(_)
| InternalSchemaType::RegularType(_)
| InternalSchemaType::NoInfo => {}
InternalSchemaType::EnumHolder(variants) => {
let schema_fields: Vec<(Cow<'static, str>, SchemaDefinition)> = variants
.iter()
.map(|variant| {
(
variant.name().into(),
SchemaTypeInfo::from(variant).to_definition(),
)
})
.collect();
schema.one_of = schema_fields
.iter()
.map(|(_, definition)| definition.schema.clone().into())
.collect();
}
InternalSchemaType::EnumVariant(variant_info) => {
schema.kind = Some(SchemaKind::Value);
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
let ty_info: TypeInformation = (&variant_info).into();
let field_data: Option<SchemaFieldData> = Some((&variant_info).into());
id = None;
match &variant_info {
VariantInfo::Struct(struct_variant_info) => {
let fields = get_fields_information(struct_variant_info.iter());
let schema_field = SchemaTypeInfo {
ty_info,
field_data,
internal_schema_type: InternalSchemaType::FieldsHolder(
FieldsInformation {
fields,
fields_type: FieldType::Named,
},
),
reflect_type_data: None,
};
let definition = schema_field.to_definition();
schema.properties =
[(variant_info.name().into(), definition.schema.into())].into();
schema.required = vec![variant_info.name().into()];
}
VariantInfo::Tuple(tuple_variant_info) => {
let stored_fields = get_fields_information(tuple_variant_info.iter());
let schema_field = SchemaTypeInfo {
ty_info,
field_data: None,
internal_schema_type: InternalSchemaType::FieldsHolder(
FieldsInformation {
fields: stored_fields,
fields_type: FieldType::Unnamed,
},
),
reflect_type_data: None,
};
let definition = schema_field.to_definition();
schema.properties =
[(variant_info.name().into(), definition.schema.into())].into();
schema.required = vec![variant_info.name().into()];
}
VariantInfo::Unit(unit_variant_info) => {
let internal_schema_type = (&ty_info).into();
let schema_field = SchemaTypeInfo {
ty_info,
field_data,
internal_schema_type,
reflect_type_data: None,
};
return Some((
None,
JsonSchemaBevyType {
const_value: Some(unit_variant_info.name().to_string().into()),
schema_type: Some(SchemaTypeVariant::Single(SchemaType::String)),
description: schema_field.get_docs(),
kind: Some(SchemaKind::Value),
..Default::default()
},
));
}
}
}
InternalSchemaType::FieldsHolder(fields) => match fields.fields_type {
FieldType::Named => {
schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
let schema_fields: Vec<(Cow<'static, str>, SchemaTypeInfo)> = fields
.fields
.iter()
.map(|field| (field.field.to_name(), SchemaTypeInfo::from(field)))
.collect();
schema.properties = schema_fields
.iter()
.map(|(name, schema)| (name.clone(), schema.to_ref_schema().into()))
.collect();
schema.required = fields
.fields
.iter()
.map(|field| field.field.to_name())
.collect();
}
FieldType::Unnamed if fields.fields.len() == 1 => {
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
let SchemaDefinition {
id,
schema: new_schema_type,
definitions: _,
dependencies: _,
} = field_schema.to_definition();
if let Some(id) = id {
definitions.insert(id.clone(), field_schema);
schema.ref_type = Some(TypeReferencePath::definition(id));
} else {
let (type_path, short_path, crate_name, module_path) =
if let Some(type_path_table) =
schema_info.ty_info.try_get_type_path_table()
{
(
type_path_table.path().into(),
type_path_table.short_path().into(),
type_path_table.crate_name().map(Into::into),
type_path_table.module_path().map(Into::into),
)
} else {
(Cow::default(), Cow::default(), None, None)
};
schema = JsonSchemaBevyType {
short_path,
type_path,
module_path,
crate_name,
kind: Some(SchemaKind::TupleStruct),
schema_type: (&schema_info).into(),
description: schema_info.get_docs(),
..new_schema_type
};
}
}
s => {
let schema_fields: Vec<SchemaTypeInfo> =
fields.fields.iter().map(SchemaTypeInfo::from).collect();
schema.prefix_items = schema_fields
.iter()
.map(|field| {
let field_schema = if s == FieldType::ForceUnnamed
&& field
.field_data
.as_ref()
.is_some_and(|f| f.description.is_none())
{
if let Some(field_data) = field.field_data.as_ref() {
let description = field_data.name.clone();
SchemaTypeInfo {
field_data: Some(SchemaFieldData {
description,
..field_data.clone()
}),
..field.clone()
}
.to_ref_schema()
} else {
field.to_ref_schema()
}
} else {
field.to_ref_schema()
};
field_schema.into()
})
.collect();
schema.min_items = Some(fields.fields.len() as u64);
schema.max_items = Some(fields.fields.len() as u64);
}
},
InternalSchemaType::Array {
element_ty,
min_size,
max_size,
} => {
id = None;
let items_schema = element_ty.to_schema_type_info();
schema.items = Some(items_schema.to_ref_schema().into());
schema.min_items = min_size;
schema.max_items = max_size;
}
InternalSchemaType::Optional {
generic,
ref schema_type_info,
} => {
let schema_optional = SchemaTypeInfo {
ty_info: TypeInformation::Type(Box::new(*generic.ty())),
..(**schema_type_info).clone()
};
schema.ref_type = None;
schema.schema_type = None;
schema.one_of = vec![
Box::new(JsonSchemaBevyType {
schema_type: Some(SchemaTypeVariant::Single(SchemaType::Null)),
..Default::default()
}),
Box::new(schema_optional.to_ref_schema()),
];
}
}
Some((id, schema))
}
fn get_type_dependencies(&self, type_id: TypeId) -> HashSet<TypeId> {
let Some(type_reg) = self.get(type_id) else {
return HashSet::new();
};
let internal_schema_type: InternalSchemaType = (&TypeInformation::from(type_reg)).into();
internal_schema_type.get_dependencies(self)
}
fn build_schema_for_type_id_with_definitions(
&self,
type_id: TypeId,
metadata: &SchemaTypesMetadata,
) -> Option<JsonSchemaBevyType> {
let Some((_, mut schema)) = self.build_schema_for_type_id(type_id, metadata) else {
return None;
};
let dependencies = self.get_type_dependencies(type_id);
eprintln!("{} -> {:#?}", schema.type_path, dependencies.len());
schema.definitions = dependencies
.into_iter()
.flat_map(|id| {
let result = self.build_schema_for_type_id(id, metadata);
let Some((Some(schema_id), schema)) = result else {
return None;
};
Some((schema_id, Box::new(schema)))
})
.collect();
Some(schema)
}
}
#[cfg(test)] #[cfg(test)]
pub(super) mod tests { pub(super) mod tests {
use bevy_ecs::{component::Component, name::Name, reflect::AppTypeRegistry}; use bevy_ecs::{component::Component, name::Name, reflect::AppTypeRegistry};
@ -2173,18 +2598,28 @@ pub(super) mod tests {
pub list: Vec<BaseStruct>, pub list: Vec<BaseStruct>,
pub optional: Option<BaseStruct>, pub optional: Option<BaseStruct>,
} }
let type_info = TypeInformation::from(&ArrayComponent::get_type_registration())
.to_schema_type_info() let atr = AppTypeRegistry::default();
.to_definition(); {
let type_info_second = let mut register = atr.write();
TypeInformation::from(&ArrayComponentWithMoreVariants::get_type_registration()) register.register::<ArrayComponent>();
.to_schema_type_info() register.register::<ArrayComponentWithMoreVariants>();
.to_definition(); }
assert_eq!( let types = atr.read();
type_info.definitions.len(), let schema = types
type_info_second.definitions.len() .build_schema_for_type_id_with_definitions(
); TypeId::of::<ArrayComponent>(),
validate::<ArrayComponentWithMoreVariants>(type_info_second.into(), &[], &[], &[]); &Default::default(),
)
.expect("");
let schema_second = types
.build_schema_for_type_id_with_definitions(
TypeId::of::<ArrayComponentWithMoreVariants>(),
&Default::default(),
)
.expect("");
assert_eq!(schema.definitions.len(), schema_second.definitions.len());
validate::<ArrayComponentWithMoreVariants>(schema_second, &[], &[], &[]);
} }
#[test] #[test]
@ -2235,12 +2670,29 @@ pub(super) mod tests {
pub second: SecondStruct, pub second: SecondStruct,
pub third: ThirdStruct, pub third: ThirdStruct,
} }
let type_info =
TypeInformation::from(&NestedStruct::get_type_registration()).to_schema_type_info(); let atr = AppTypeRegistry::default();
let s: JsonSchemaBevyType = type_info.to_definition().into(); {
assert_eq!(s.definitions.len(), 3); let mut register = atr.write();
register.register::<NestedStruct>();
}
let types = atr.read();
let schema = types
.build_schema_for_type_id_with_definitions(
TypeId::of::<NestedStruct>(),
&Default::default(),
)
.expect("");
assert_eq!(
schema.definitions.len(),
3,
"Expected 3 definitions, but got {}, schema: {}",
schema.definitions.len(),
serde_json::to_string_pretty(&schema).unwrap_or_default()
);
validate::<NestedStruct>( validate::<NestedStruct>(
s, schema,
&[NestedStruct { &[NestedStruct {
other: OtherStruct { field: "s".into() }, other: OtherStruct { field: "s".into() },
..Default::default() ..Default::default()