Schema definitions generate better

This commit is contained in:
Piotr Siuszko 2025-07-03 11:40:27 +02:00
parent 5ea21b4705
commit d83f82f045
2 changed files with 207 additions and 62 deletions

View File

@ -82,12 +82,12 @@ impl TryFrom<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
if let Some(s) = reg.data::<ReflectJsonSchema>() { if let Some(s) = reg.data::<ReflectJsonSchema>() {
return Ok(s.0.clone()); return Ok(s.0.clone());
} }
let (_, mut typed_schema) = TypeInformation::from(reg) let mut schema: JsonSchemaBevyType = TypeInformation::from(reg)
.to_schema_type_info() .to_schema_type_info()
.to_definition(); .to_definition()
.into();
typed_schema.reflect_type_data = metadata.get_registered_reflect_types(reg); schema.reflect_type_data = metadata.get_registered_reflect_types(reg);
Ok(typed_schema) Ok(schema)
} }
} }

View File

@ -2,6 +2,7 @@
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_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,
@ -1027,6 +1028,32 @@ pub struct SchemaTypeInfo {
pub stored_fields: Option<FieldsInformation>, pub stored_fields: Option<FieldsInformation>,
} }
/// Contains comprehensive information about a type's schema representation.
/// This struct aggregates all the necessary information to generate a JSON schema
/// from Rust type information obtained through reflection.
#[derive(Clone, Debug, Default)]
pub struct SchemaDefinition {
/// The type reference ID of the schema.
pub id: Option<TypeReferenceId>,
/// The JSON schema type of the schema.
pub schema: JsonSchemaBevyType,
/// The properties of the schema.
pub definitions: HashMap<TypeReferenceId, SchemaTypeInfo>,
}
impl From<SchemaDefinition> for JsonSchemaBevyType {
fn from(value: SchemaDefinition) -> Self {
Self {
definitions: value
.definitions
.iter()
.map(|(id, schema)| (id.clone(), schema.to_definition().schema.into()))
.collect(),
..value.schema
}
}
}
impl SchemaTypeInfo { impl SchemaTypeInfo {
/// Get the documentation for the schema type. /// Get the documentation for the schema type.
/// If the field has a description, it is returned. /// If the field has a description, it is returned.
@ -1063,23 +1090,57 @@ impl SchemaTypeInfo {
pub fn to_ref_schema(&self) -> JsonSchemaBevyType { pub fn to_ref_schema(&self) -> JsonSchemaBevyType {
let range = self.get_range(); let range = self.get_range();
let description = self.get_docs(); let description = self.get_docs();
let internal_type: InternalSchemaType = (self).into();
let (ref_type, schema_type) = (self.ty_info.get_reference_type(), self.into()); let (ref_type, schema_type) = (self.ty_info.get_reference_type(), self.into());
JsonSchemaBevyType {
let mut schema = JsonSchemaBevyType {
description, description,
minimum: range.min.get_inclusive(), minimum: range.min.get_inclusive(),
maximum: range.max.get_inclusive(), maximum: range.max.get_inclusive(),
exclusive_minimum: range.min.get_exclusive(), exclusive_minimum: range.min.get_exclusive(),
exclusive_maximum: range.max.get_exclusive(), exclusive_maximum: range.max.get_exclusive(),
ref_type,
kind: None, kind: None,
ref_type,
schema_type, schema_type,
..default() ..default()
};
match internal_type {
InternalSchemaType::Array {
element_ty,
min_size,
max_size,
} => {
schema.ref_type = None;
let items_schema = SchemaTypeInfo {
ty_info: element_ty.clone(),
field_data: None,
stored_fields: None,
};
schema.items = Some(items_schema.to_ref_schema().into());
schema.min_items = min_size;
schema.max_items = max_size;
}
InternalSchemaType::EnumHolder(_)
| InternalSchemaType::EnumVariant(_)
| InternalSchemaType::FieldsHolder(_)
| InternalSchemaType::Optional {
generic: _,
schema_type_info: _,
}
| InternalSchemaType::Map { key: _, value: _ } => {
schema.ref_type = None;
}
_ => {}
} }
schema
} }
/// Converts the schema type information into a JSON schema definition. /// Converts the schema type information into a JSON schema definition.
pub fn to_definition(&self) -> (Option<TypeReferenceId>, JsonSchemaBevyType) { pub fn to_definition(&self) -> SchemaDefinition {
let id: Option<TypeReferenceId> = self.ty_info.try_get_type_path_table().map(Into::into); let mut id: Option<TypeReferenceId> =
self.ty_info.try_get_type_path_table().map(Into::into);
let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new();
let range = self.ty_info.get_range(); let range = self.ty_info.get_range();
let (type_path, short_path, crate_name, module_path) = let (type_path, short_path, crate_name, module_path) =
@ -1120,22 +1181,57 @@ impl SchemaTypeInfo {
field_data: None, field_data: None,
stored_fields: None, stored_fields: None,
}; };
schema.additional_properties = Some(key.clone().to_definition().1.into()); if !key.ty_info.is_primitive_type() {
schema.value_type = Some(value.to_definition().1.into()); let SchemaDefinition {
schema.key_type = Some(key.to_definition().1.into()); id,
schema: _,
definitions: field_definitions,
} = key.to_definition();
if let Some(id) = id {
definitions.insert(id, key.clone());
definitions.extend(field_definitions);
}
}
if !value.ty_info.is_primitive_type() {
let SchemaDefinition {
id,
schema: _,
definitions: field_definitions,
} = value.to_definition();
if let Some(id) = id {
definitions.insert(id, value.clone());
definitions.extend(field_definitions);
}
}
schema.additional_properties = Some(key.to_ref_schema().into());
schema.value_type = Some(value.to_ref_schema().into());
schema.key_type = Some(key.to_ref_schema().into());
} }
InternalSchemaType::Regular(_) InternalSchemaType::Regular(_)
| InternalSchemaType::RegularType(_) | InternalSchemaType::RegularType(_)
| InternalSchemaType::NoInfo => {} | InternalSchemaType::NoInfo => {}
InternalSchemaType::EnumHolder(variants) => { InternalSchemaType::EnumHolder(variants) => {
schema.one_of = variants.iter().map(build_schema).collect(); 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();
definitions.extend(schema_fields.iter().flat_map(|s| s.1.definitions.clone()));
} }
InternalSchemaType::EnumVariant(variant_info) => { InternalSchemaType::EnumVariant(variant_info) => {
schema.kind = Some(SchemaKind::Value); schema.kind = Some(SchemaKind::Value);
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object)); schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
let ty_info: TypeInformation = (&variant_info).into(); let ty_info: TypeInformation = (&variant_info).into();
let field_data: Option<SchemaFieldData> = Some((&variant_info).into()); let field_data: Option<SchemaFieldData> = Some((&variant_info).into());
id = None;
match &variant_info { match &variant_info {
VariantInfo::Struct(struct_variant_info) => { VariantInfo::Struct(struct_variant_info) => {
let fields = get_fields_information(struct_variant_info.iter()); let fields = get_fields_information(struct_variant_info.iter());
@ -1148,13 +1244,12 @@ impl SchemaTypeInfo {
fields_type: FieldType::Named, fields_type: FieldType::Named,
}), }),
}; };
let definition = schema_field.to_definition();
schema.properties = [( schema.properties =
variant_info.name().into(), [(variant_info.name().into(), definition.schema.into())].into();
schema_field.to_definition().1.into(),
)]
.into();
schema.required = vec![variant_info.name().into()]; schema.required = vec![variant_info.name().into()];
definitions.extend(definition.definitions);
} }
VariantInfo::Tuple(tuple_variant_info) => { VariantInfo::Tuple(tuple_variant_info) => {
let stored_fields = get_fields_information(tuple_variant_info.iter()); let stored_fields = get_fields_information(tuple_variant_info.iter());
@ -1167,12 +1262,11 @@ impl SchemaTypeInfo {
fields_type: FieldType::Unnamed, fields_type: FieldType::Unnamed,
}), }),
}; };
let definition = schema_field.to_definition();
schema.properties = [( schema.properties =
variant_info.name().into(), [(variant_info.name().into(), definition.schema.into())].into();
schema_field.to_definition().1.into(), definitions.extend(definition.definitions);
)]
.into();
schema.required = vec![variant_info.name().into()]; schema.required = vec![variant_info.name().into()];
} }
VariantInfo::Unit(unit_variant_info) => { VariantInfo::Unit(unit_variant_info) => {
@ -1181,16 +1275,17 @@ impl SchemaTypeInfo {
field_data, field_data,
stored_fields: None, stored_fields: None,
}; };
return ( return SchemaDefinition {
None, id: None,
JsonSchemaBevyType { schema: JsonSchemaBevyType {
const_value: Some(unit_variant_info.name().to_string().into()), const_value: Some(unit_variant_info.name().to_string().into()),
schema_type: Some(SchemaTypeVariant::Single(SchemaType::String)), schema_type: Some(SchemaTypeVariant::Single(SchemaType::String)),
description: schema_field.get_docs(), description: schema_field.get_docs(),
kind: Some(SchemaKind::Value), kind: Some(SchemaKind::Value),
..Default::default() ..Default::default()
}, },
); definitions: HashMap::new(),
};
} }
} }
} }
@ -1211,11 +1306,16 @@ impl SchemaTypeInfo {
if field_schema.ty_info.is_primitive_type() { if field_schema.ty_info.is_primitive_type() {
continue; continue;
} }
let (id, definition) = field_schema.to_definition(); let SchemaDefinition {
id,
schema: _,
definitions: field_definitions,
} = field_schema.to_definition();
let Some(id) = id else { continue }; let Some(id) = id else { continue };
if !schema.definitions.contains_key(&id) { if !definitions.contains_key(&id) {
schema.definitions.insert(id, definition.into()); definitions.insert(id, field_schema);
} }
definitions.extend(field_definitions);
} }
schema.required = fields schema.required = fields
.fields .fields
@ -1225,17 +1325,40 @@ impl SchemaTypeInfo {
} }
FieldType::Unnamed => { FieldType::Unnamed => {
if fields.fields.len() == 1 { if fields.fields.len() == 1 {
let (_, new_schema_type) = let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
SchemaTypeInfo::from(&fields.fields[0]).to_definition(); let SchemaDefinition {
id,
schema: new_schema_type,
definitions: _,
} = field_schema.to_definition();
if let Some(id) = id {
definitions.insert(id, field_schema);
}
schema = new_schema_type; schema = new_schema_type;
schema.schema_type = self.into(); schema.schema_type = self.into();
schema.description = self.get_docs(); schema.description = self.get_docs();
} else { } else {
schema.prefix_items = fields let schema_fields: Vec<SchemaTypeInfo> =
.fields fields.fields.iter().map(SchemaTypeInfo::from).collect();
schema.prefix_items = schema_fields
.iter() .iter()
.map(|field| SchemaTypeInfo::from(field).to_definition().1.into()) .map(|field| field.to_ref_schema().into())
.collect(); .collect();
for field_schema in schema_fields {
if field_schema.ty_info.is_primitive_type() {
continue;
}
let SchemaDefinition {
id,
schema: _,
definitions: field_definitions,
} = field_schema.to_definition();
let Some(id) = id else { continue };
if !definitions.contains_key(&id) {
definitions.insert(id, field_schema);
}
definitions.extend(field_definitions);
}
schema.min_items = Some(fields.fields.len() as u64); schema.min_items = Some(fields.fields.len() as u64);
schema.max_items = Some(fields.fields.len() as u64); schema.max_items = Some(fields.fields.len() as u64);
} }
@ -1246,35 +1369,53 @@ impl SchemaTypeInfo {
min_size, min_size,
max_size, max_size,
} => { } => {
id = None;
let items_schema = SchemaTypeInfo { let items_schema = SchemaTypeInfo {
ty_info: element_ty.clone(), ty_info: element_ty.clone(),
field_data: None, field_data: None,
stored_fields: None, stored_fields: None,
}; };
schema.items = Some(items_schema.to_definition().1.into()); schema.items = Some(items_schema.to_ref_schema().into());
schema.min_items = min_size; schema.min_items = min_size;
schema.max_items = max_size; schema.max_items = max_size;
}
InternalSchemaType::Optional { if !items_schema.ty_info.is_primitive_type() {
generic: _, let SchemaDefinition {
ref schema_type_info, id,
} => { schema: _,
let schema_variant: JsonSchemaVariant = definitions: field_definitions,
schema_type_info.as_ref().clone().to_definition().1.into(); } = items_schema.to_definition();
if let JsonSchemaVariant::Schema(value) = schema_variant { if let Some(id) = id {
let range = self.get_range(); definitions.insert(id, items_schema);
schema = *value; definitions.extend(field_definitions);
schema.schema_type = self.into(); }
schema.minimum = range.min.get_inclusive();
schema.maximum = range.max.get_inclusive();
schema.exclusive_minimum = range.min.get_exclusive();
schema.exclusive_maximum = range.max.get_exclusive();
schema.description = self.get_docs();
schema.kind = Some(SchemaKind::Optional);
} }
} }
InternalSchemaType::Optional {
generic,
ref schema_type_info,
} => {
let schema_optional = SchemaTypeInfo {
ty_info: TypeInformation::Type(Box::new(*generic.ty())),
..(**schema_type_info).clone()
};
let optional_def = schema_optional.to_definition();
let range = schema_optional.get_range();
schema = optional_def.schema;
schema.schema_type = self.into();
schema.minimum = range.min.get_inclusive();
schema.maximum = range.max.get_inclusive();
schema.exclusive_minimum = range.min.get_exclusive();
schema.exclusive_maximum = range.max.get_exclusive();
schema.description = self.get_docs();
schema.kind = Some(SchemaKind::Optional);
}
}
SchemaDefinition {
id,
schema,
definitions,
} }
(id, schema)
} }
} }
@ -1388,7 +1529,7 @@ where
SchemaTypeInfo: From<&'a T>, SchemaTypeInfo: From<&'a T>,
{ {
let schema: SchemaTypeInfo = value.into(); let schema: SchemaTypeInfo = value.into();
schema.to_definition().1.into() schema.to_definition().schema.into()
} }
impl From<&UnnamedField> for SchemaFieldData { impl From<&UnnamedField> for SchemaFieldData {
@ -1604,7 +1745,7 @@ mod tests {
.expect(""), .expect(""),
) )
.to_schema_type_info(); .to_schema_type_info();
let _: JsonSchemaVariant = declaration.to_definition().1.into(); let _: JsonSchemaBevyType = declaration.to_definition().into();
} }
#[test] #[test]
@ -1657,7 +1798,7 @@ mod tests {
} }
let type_info = let type_info =
TypeInformation::from(&ArrayComponent::get_type_registration()).to_schema_type_info(); TypeInformation::from(&ArrayComponent::get_type_registration()).to_schema_type_info();
let _: JsonSchemaVariant = type_info.to_definition().1.into(); let _: JsonSchemaBevyType = type_info.to_definition().into();
} }
#[test] #[test]
@ -1672,7 +1813,7 @@ mod tests {
.is_ok()); .is_ok());
let type_info = let type_info =
TypeInformation::from(&HashMapStruct::get_type_registration()).to_schema_type_info(); TypeInformation::from(&HashMapStruct::get_type_registration()).to_schema_type_info();
let _: JsonSchemaVariant = type_info.to_definition().1.into(); let _: JsonSchemaBevyType = type_info.to_definition().into();
} }
#[test] #[test]
@ -1689,8 +1830,8 @@ mod tests {
#[derive(Reflect, Default, Deserialize, Serialize)] #[derive(Reflect, Default, Deserialize, Serialize)]
pub struct ThirdStruct { pub struct ThirdStruct {
pub array_strings: Vec<String>, pub array_strings: Vec<String>,
pub array_structs: Vec<OtherStruct>, pub array_structs: [OtherStruct; 5],
pub map_strings: HashMap<String, String>, pub map_strings: HashMap<String, i32>,
} }
#[derive(Reflect, Default, Deserialize, Serialize)] #[derive(Reflect, Default, Deserialize, Serialize)]
pub struct NestedStruct { pub struct NestedStruct {
@ -1700,7 +1841,11 @@ mod tests {
} }
let type_info = let type_info =
TypeInformation::from(&NestedStruct::get_type_registration()).to_schema_type_info(); TypeInformation::from(&NestedStruct::get_type_registration()).to_schema_type_info();
let _s: JsonSchemaVariant = type_info.to_definition().1.into(); let _s: JsonSchemaBevyType = type_info.to_definition().into();
// eprintln!("{}", serde_json::to_string_pretty(&s).expect("msg")); // eprintln!("{}", serde_json::to_string_pretty(&s).expect("msg"));
// eprintln!(
// "{}",
// serde_json::to_string_pretty(&NestedStruct::default()).expect("msg")
// );
} }
} }