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>() {
return Ok(s.0.clone());
}
let (_, mut typed_schema) = TypeInformation::from(reg)
let mut schema: JsonSchemaBevyType = TypeInformation::from(reg)
.to_schema_type_info()
.to_definition();
typed_schema.reflect_type_data = metadata.get_registered_reflect_types(reg);
Ok(typed_schema)
.to_definition()
.into();
schema.reflect_type_data = metadata.get_registered_reflect_types(reg);
Ok(schema)
}
}

View File

@ -2,6 +2,7 @@
use alloc::borrow::Cow;
use alloc::sync::Arc;
use bevy_derive::{Deref, DerefMut};
use bevy_platform::collections::HashMap;
use bevy_reflect::attributes::CustomAttributes;
use bevy_reflect::{
EnumInfo, GenericInfo, NamedField, Reflect, Type, TypeInfo, TypePathTable, TypeRegistration,
@ -1027,6 +1028,32 @@ pub struct SchemaTypeInfo {
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 {
/// Get the documentation for the schema type.
/// If the field has a description, it is returned.
@ -1063,23 +1090,57 @@ impl SchemaTypeInfo {
pub fn to_ref_schema(&self) -> JsonSchemaBevyType {
let range = self.get_range();
let description = self.get_docs();
let internal_type: InternalSchemaType = (self).into();
let (ref_type, schema_type) = (self.ty_info.get_reference_type(), self.into());
JsonSchemaBevyType {
let mut schema = JsonSchemaBevyType {
description,
minimum: range.min.get_inclusive(),
maximum: range.max.get_inclusive(),
exclusive_minimum: range.min.get_exclusive(),
exclusive_maximum: range.max.get_exclusive(),
ref_type,
kind: None,
ref_type,
schema_type,
..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.
pub fn to_definition(&self) -> (Option<TypeReferenceId>, JsonSchemaBevyType) {
let id: Option<TypeReferenceId> = self.ty_info.try_get_type_path_table().map(Into::into);
pub fn to_definition(&self) -> SchemaDefinition {
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 (type_path, short_path, crate_name, module_path) =
@ -1120,22 +1181,57 @@ impl SchemaTypeInfo {
field_data: None,
stored_fields: None,
};
schema.additional_properties = Some(key.clone().to_definition().1.into());
schema.value_type = Some(value.to_definition().1.into());
schema.key_type = Some(key.to_definition().1.into());
if !key.ty_info.is_primitive_type() {
let SchemaDefinition {
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::RegularType(_)
| InternalSchemaType::NoInfo => {}
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) => {
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());
@ -1148,13 +1244,12 @@ impl SchemaTypeInfo {
fields_type: FieldType::Named,
}),
};
let definition = schema_field.to_definition();
schema.properties = [(
variant_info.name().into(),
schema_field.to_definition().1.into(),
)]
.into();
schema.properties =
[(variant_info.name().into(), definition.schema.into())].into();
schema.required = vec![variant_info.name().into()];
definitions.extend(definition.definitions);
}
VariantInfo::Tuple(tuple_variant_info) => {
let stored_fields = get_fields_information(tuple_variant_info.iter());
@ -1167,12 +1262,11 @@ impl SchemaTypeInfo {
fields_type: FieldType::Unnamed,
}),
};
let definition = schema_field.to_definition();
schema.properties = [(
variant_info.name().into(),
schema_field.to_definition().1.into(),
)]
.into();
schema.properties =
[(variant_info.name().into(), definition.schema.into())].into();
definitions.extend(definition.definitions);
schema.required = vec![variant_info.name().into()];
}
VariantInfo::Unit(unit_variant_info) => {
@ -1181,16 +1275,17 @@ impl SchemaTypeInfo {
field_data,
stored_fields: None,
};
return (
None,
JsonSchemaBevyType {
return SchemaDefinition {
id: None,
schema: 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()
},
);
definitions: HashMap::new(),
};
}
}
}
@ -1211,11 +1306,16 @@ impl SchemaTypeInfo {
if field_schema.ty_info.is_primitive_type() {
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 };
if !schema.definitions.contains_key(&id) {
schema.definitions.insert(id, definition.into());
if !definitions.contains_key(&id) {
definitions.insert(id, field_schema);
}
definitions.extend(field_definitions);
}
schema.required = fields
.fields
@ -1225,17 +1325,40 @@ impl SchemaTypeInfo {
}
FieldType::Unnamed => {
if fields.fields.len() == 1 {
let (_, new_schema_type) =
SchemaTypeInfo::from(&fields.fields[0]).to_definition();
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
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.schema_type = self.into();
schema.description = self.get_docs();
} else {
schema.prefix_items = fields
.fields
let schema_fields: Vec<SchemaTypeInfo> =
fields.fields.iter().map(SchemaTypeInfo::from).collect();
schema.prefix_items = schema_fields
.iter()
.map(|field| SchemaTypeInfo::from(field).to_definition().1.into())
.map(|field| field.to_ref_schema().into())
.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.max_items = Some(fields.fields.len() as u64);
}
@ -1246,35 +1369,53 @@ impl SchemaTypeInfo {
min_size,
max_size,
} => {
id = None;
let items_schema = SchemaTypeInfo {
ty_info: element_ty.clone(),
field_data: 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.max_items = max_size;
}
InternalSchemaType::Optional {
generic: _,
ref schema_type_info,
} => {
let schema_variant: JsonSchemaVariant =
schema_type_info.as_ref().clone().to_definition().1.into();
if let JsonSchemaVariant::Schema(value) = schema_variant {
let range = self.get_range();
schema = *value;
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);
if !items_schema.ty_info.is_primitive_type() {
let SchemaDefinition {
id,
schema: _,
definitions: field_definitions,
} = items_schema.to_definition();
if let Some(id) = id {
definitions.insert(id, items_schema);
definitions.extend(field_definitions);
}
}
}
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>,
{
let schema: SchemaTypeInfo = value.into();
schema.to_definition().1.into()
schema.to_definition().schema.into()
}
impl From<&UnnamedField> for SchemaFieldData {
@ -1604,7 +1745,7 @@ mod tests {
.expect(""),
)
.to_schema_type_info();
let _: JsonSchemaVariant = declaration.to_definition().1.into();
let _: JsonSchemaBevyType = declaration.to_definition().into();
}
#[test]
@ -1657,7 +1798,7 @@ mod tests {
}
let 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]
@ -1672,7 +1813,7 @@ mod tests {
.is_ok());
let 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]
@ -1689,8 +1830,8 @@ mod tests {
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct ThirdStruct {
pub array_strings: Vec<String>,
pub array_structs: Vec<OtherStruct>,
pub map_strings: HashMap<String, String>,
pub array_structs: [OtherStruct; 5],
pub map_strings: HashMap<String, i32>,
}
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct NestedStruct {
@ -1700,7 +1841,11 @@ mod tests {
}
let 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(&NestedStruct::default()).expect("msg")
// );
}
}