schema IDs using URN format, further improvements, tests for export

endpoint
This commit is contained in:
Piotr Siuszko 2025-07-03 17:08:42 +02:00
parent 16c7db42aa
commit 9ed0787d41
4 changed files with 434 additions and 134 deletions

View File

@ -26,8 +26,9 @@ use serde_json::{Map, Value};
use crate::{
error_codes,
schemas::{
json_schema::{JsonSchemaBevyType, TypeRegistrySchemaReader},
json_schema::{JsonSchemaBevyType, SchemaMarker, TypeRegistrySchemaReader},
open_rpc::OpenRpcDocument,
reflect_info::TypeInformation,
},
BrpError, BrpResult,
};
@ -362,6 +363,22 @@ pub struct BrpJsonSchemaQueryFilter {
pub type_limit: JsonSchemaTypeLimit,
}
impl BrpJsonSchemaQueryFilter {
/// 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 {
let Some(crate_name) = type_registration.type_info().type_path_table().crate_name() else {
return false;
};
if !self.with_crates.is_empty() && !self.with_crates.iter().any(|c| crate_name.eq(c)) {
return true;
}
if !self.without_crates.is_empty() && self.without_crates.iter().any(|c| crate_name.eq(c)) {
return true;
}
false
}
}
/// Additional [`BrpJsonSchemaQueryFilter`] constraints that can be placed on a query to include or exclude
/// certain definitions.
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
@ -375,6 +392,34 @@ pub struct JsonSchemaTypeLimit {
pub with: Vec<String>,
}
impl JsonSchemaTypeLimit {
/// Check if the type limit is empty.
pub fn is_empty(&self) -> bool {
self.without.is_empty() && self.with.is_empty()
}
/// Check if the type limit should skip a type.
pub fn should_skip_type(&self, registered_types: &[Cow<'_, str>]) -> bool {
if !self.with.is_empty()
&& !self
.with
.iter()
.any(|c| registered_types.iter().any(|cc| c.eq(cc)))
{
return true;
}
if !self.without.is_empty()
&& self
.without
.iter()
.any(|c| registered_types.iter().any(|cc| c.eq(cc)))
{
return true;
}
false
}
}
/// A response from the world to the client that specifies a single entity.
///
/// This is sent in response to `bevy/spawn`.
@ -1230,48 +1275,47 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>();
let types = world.resource::<AppTypeRegistry>();
let types = types.read();
let schemas = types
let mut schema = JsonSchemaBevyType {
schema: Some(SchemaMarker.into()),
description: Some(
"This schema represents the types registered in the Bevy application.".into(),
),
..Default::default()
};
schema.definitions = types
.iter()
.filter_map(|type_reg| {
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()
&& !filter.with_crates.iter().any(|c| crate_name.eq(c))
{
return None;
}
if !filter.without_crates.is_empty()
&& filter.without_crates.iter().any(|c| crate_name.eq(c))
if filter.should_skip_for_crate(type_reg) {
return None;
}
if !filter.type_limit.is_empty() {
let registered_types = extra_info.get_registered_reflect_types(type_reg);
if filter
.type_limit
.should_skip_type(registered_types.as_slice())
{
return None;
}
}
let id = type_reg.type_id();
let schema = types.export_type_json_schema_for_id(id, extra_info)?;
if !filter.type_limit.with.is_empty()
&& !filter
.type_limit
.with
.iter()
.any(|c| schema.reflect_type_data.iter().any(|cc| c.eq(cc)))
{
return None;
}
if !filter.type_limit.without.is_empty()
&& filter
.type_limit
.without
.iter()
.any(|c| schema.reflect_type_data.iter().any(|cc| c.eq(cc)))
{
return None;
}
Some((type_reg.type_info().type_path().into(), schema))
let schema_type_info = TypeInformation::TypeRegistration(type_reg.clone())
.to_schema_type_info_with_metadata(extra_info);
let schema_id = schema_type_info.ty_info.try_get_type_reference_id()?;
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)
})
.collect::<HashMap<Cow<'static, str>, JsonSchemaBevyType>>();
.flatten()
.collect();
serde_json::to_value(schemas).map_err(BrpError::internal)
serde_json::to_value(schema).map_err(BrpError::internal)
}
/// Immutably retrieves an entity from the [`World`], returning an error if the
@ -1533,6 +1577,12 @@ mod tests {
);
}
use bevy_ecs::component::Component;
use bevy_math::Vec3;
use bevy_reflect::Reflect;
use crate::schemas::{ReflectJsonSchemaForceAsArray, SchemaTypesMetadata};
use super::*;
#[test]
@ -1556,4 +1606,96 @@ mod tests {
entity: Entity::from_raw_u32(0).unwrap(),
});
}
#[test]
fn export_schema_test() {
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct OtherStruct {
/// FIELD DOC
pub field: String,
}
/// STRUCT DOC
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct SecondStruct {
pub field: String,
/// FIELD DOC
pub other: OtherStruct,
}
/// STRUCT DOC
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct ThirdStruct {
pub array_strings: Vec<String>,
pub array_structs: [OtherStruct; 5],
pub map_strings: HashMap<String, i32>,
}
#[derive(Reflect, Default, Deserialize, Serialize, Component)]
#[reflect(Component)]
pub struct NestedStruct {
pub other: OtherStruct,
pub second: SecondStruct,
/// DOC FOR FIELD
pub third: ThirdStruct,
}
let mut world = World::new();
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<NestedStruct>();
register.register::<Vec3>();
register.register_type_data::<Vec3, ReflectJsonSchemaForceAsArray>();
}
world.insert_resource(atr);
world.insert_resource(SchemaTypesMetadata::default());
let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world);
assert_eq!(response.definitions.len(), 5);
let response = export_registry_types_ext(
BrpJsonSchemaQueryFilter {
without_crates: vec!["bevy_remote".to_string()],
..Default::default()
},
&world,
);
assert_eq!(response.definitions.len(), 1);
{
let first = response.definitions.iter().next().expect("Should have one");
assert_eq!(first.1.id, "urn:bevy:glam-Vec3");
}
let response = export_registry_types_ext(
BrpJsonSchemaQueryFilter {
with_crates: vec!["bevy_remote".to_string()],
..Default::default()
},
&world,
);
assert_eq!(response.definitions.len(), 4);
let response = export_registry_types_ext(
BrpJsonSchemaQueryFilter {
type_limit: JsonSchemaTypeLimit {
with: vec!["Component".to_string()],
..Default::default()
},
..Default::default()
},
&world,
);
assert_eq!(response.definitions.len(), 4);
}
fn export_registry_types_ext(
input: BrpJsonSchemaQueryFilter,
world: &World,
) -> JsonSchemaBevyType {
let response_json = export_registry_types(
In(Some(
serde_json::to_value(&input).expect("Failed to serialize input"),
)),
world,
)
.expect("Failed to export registry types");
serde_json::from_value::<JsonSchemaBevyType>(response_json)
.expect("Failed to deserialize response")
}
}

View File

@ -12,7 +12,7 @@ use serde_json::Value;
use crate::schemas::{
reflect_info::{SchemaNumber, TypeInformation, TypeReferenceId, TypeReferencePath},
ReflectJsonSchema, SchemaTypesMetadata,
SchemaTypesMetadata,
};
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
@ -43,7 +43,7 @@ impl TypeRegistrySchemaReader for TypeRegistry {
) -> Option<JsonSchemaBevyType> {
let type_reg = self.get(type_id)?;
let mut schema: JsonSchemaBevyType = (type_reg, extra_info).try_into().ok()?;
schema.schema = Some(SchemaMarker::default());
schema.schema = Some(SchemaMarker.into());
schema.default_value = self.try_get_default_value_for_type_id(type_id);
Some(schema)
@ -79,11 +79,11 @@ impl TryFrom<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
fn try_from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Result<Self, Self::Error> {
let (reg, metadata) = value;
if let Some(s) = reg.data::<ReflectJsonSchema>() {
return Ok(s.0.clone());
}
// if let Some(s) = reg.data::<ReflectJsonSchema>() {
// return Ok(s.0.clone());
// }
let mut schema: JsonSchemaBevyType = TypeInformation::from(reg)
.to_schema_type_info()
.to_schema_type_info_with_metadata(metadata)
.to_definition()
.into();
schema.reflect_type_data = metadata.get_registered_reflect_types(reg);
@ -101,11 +101,21 @@ impl TryFrom<&TypeRegistration> for JsonSchemaBevyType {
/// Identifies the JSON Schema version used in the schema.
#[derive(Deserialize, Serialize, Debug, Reflect, PartialEq, Clone)]
pub struct SchemaMarker(Cow<'static, str>);
pub struct SchemaMarker;
impl Default for SchemaMarker {
fn default() -> Self {
Self("https://json-schema.org/draft/2020-12/schema".into())
impl SchemaMarker {
const DEFAULT_SCHEMA: &'static str = "https://json-schema.org/draft/2020-12/schema";
}
impl From<SchemaMarker> for &'static str {
fn from(_: SchemaMarker) -> Self {
SchemaMarker::DEFAULT_SCHEMA
}
}
impl From<SchemaMarker> for Cow<'static, str> {
fn from(_: SchemaMarker) -> Self {
Cow::Borrowed(SchemaMarker::DEFAULT_SCHEMA)
}
}
@ -116,10 +126,15 @@ impl Default for SchemaMarker {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, Reflect)]
#[serde(rename_all = "camelCase")]
pub struct JsonSchemaBevyType {
/// JSON Schema specific field.
/// This keyword declares an identifier for the schema resource.
#[serde(skip_serializing_if = "str::is_empty", default)]
#[serde(rename = "$id")]
pub id: Cow<'static, str>,
/// Identifies the JSON Schema version used in the schema.
#[serde(rename = "$schema")]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub schema: Option<SchemaMarker>,
pub schema: Option<Cow<'static, str>>,
/// JSON Schema specific field.
/// This keyword is used to reference a statically identified schema.
#[serde(rename = "$ref")]
@ -245,7 +260,7 @@ pub struct JsonSchemaBevyType {
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
#[reflect(ignore)]
#[serde(rename = "$defs")]
pub definitions: HashMap<TypeReferenceId, JsonSchemaVariant>,
pub definitions: HashMap<TypeReferenceId, Box<JsonSchemaBevyType>>,
}
/// Represents different types of JSON Schema values that can be used in schema definitions.
@ -411,7 +426,9 @@ impl SchemaType {
#[cfg(test)]
mod tests {
use crate::schemas::open_rpc::OpenRpcDocument;
use crate::schemas::reflect_info::ReferenceLocation;
use crate::schemas::ReflectJsonSchema;
use super::*;
use bevy_ecs::prelude::ReflectComponent;
@ -587,6 +604,35 @@ mod tests {
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
}
#[test]
fn reflect_struct_with_custom_schema() {
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<OpenRpcDocument>();
register.register_type_data::<OpenRpcDocument, ReflectJsonSchema>();
}
let type_registry = atr.read();
let schema = type_registry
.export_type_json_schema::<OpenRpcDocument>(&SchemaTypesMetadata::default())
.expect("Failed to export schema");
assert_eq!(
schema.ref_type,
Some(TypeReferencePath::new_ref(
ReferenceLocation::Url,
"raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json",
))
);
assert_eq!(
schema.description,
Some(
"Represents an `OpenRPC` document as defined by the `OpenRPC` specification."
.into()
)
);
assert!(schema.properties.is_empty());
}
#[test]
fn reflect_export_with_custom_schema() {
#[derive(Reflect, Component)]
@ -666,6 +712,7 @@ mod tests {
let schema = export_type::<Foo>();
let schema_as_value = serde_json::to_value(&schema).expect("Failed to serialize schema");
let mut value = json!({
"$id": "urn:bevy:bevy_remote-schemas-json_schema-tests-Foo",
"shortPath": "Foo",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"typePath": "bevy_remote::schemas::json_schema::tests::Foo",

View File

@ -20,7 +20,7 @@ pub mod reflect_info;
/// Holds mapping of reflect [type data](TypeData) to human-readable type names,
/// later on used in Bevy Json Schema.
#[derive(Debug, Resource, Reflect)]
#[derive(Debug, Resource, Reflect, Clone)]
#[reflect(Resource)]
pub struct SchemaTypesMetadata {
/// Type Data id mapping to human-readable type names.

View File

@ -24,7 +24,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::schemas::json_schema::{
JsonSchemaBevyType, JsonSchemaVariant, SchemaKind, SchemaType, SchemaTypeVariant,
};
use crate::schemas::ReflectJsonSchemaForceAsArray;
use crate::schemas::{ReflectJsonSchemaForceAsArray, SchemaTypesMetadata};
#[derive(
Debug,
@ -90,6 +90,8 @@ pub enum FieldType {
/// Unnamed field type.
#[default]
Unnamed,
/// Named field type that is stored as unnamed. Example: glam Vec3.
ForceUnnamed,
}
/// Information about the attributes of a field.
@ -109,7 +111,7 @@ impl From<&TypeInformation> for Option<FieldsInformation> {
TypeInfo::Struct(struct_info) => (
get_fields_information(struct_info.iter()),
if value.is_forced_as_array() {
FieldType::Unnamed
FieldType::ForceUnnamed
} else {
FieldType::Named
},
@ -200,13 +202,34 @@ pub enum TypeInformation {
}
impl TypeInformation {
/// Returns the reference type if the stored type is a reference one.
pub fn get_reference_type(&self) -> Option<TypeReferencePath> {
if self.is_primitive_type() {
None
/// Checks for custom schema.
pub fn try_get_custom_schema(&self) -> Option<&super::ReflectJsonSchema> {
if let Self::TypeRegistration(reg) = self {
reg.data::<super::ReflectJsonSchema>()
} else {
self.try_get_type_path_table()
.map(TypeReferencePath::definition)
None
}
}
/// Builds a `TypeReferenceId` from the type path.
pub fn try_get_type_reference_id(&self) -> Option<TypeReferenceId> {
if let Some(schema) = self.try_get_custom_schema() {
if schema.0.id.is_empty() {
None
} else {
Some(schema.0.id.trim().into())
}
} else if self.is_primitive_type() {
None
} else if let Some(optional) = self.try_get_optional_info() {
Some(optional.type_path().into())
} else if let Some(s) = self.try_get_type_info() {
if s.as_array().is_ok() || s.as_list().is_ok() || s.as_map().is_ok() {
None
} else {
self.try_get_type_path_table().map(|t| t.path().into())
}
} else {
self.try_get_type_path_table().map(|t| t.path().into())
}
}
/// Returns true if the stored type is a primitive one.
@ -224,6 +247,25 @@ impl TypeInformation {
ty_info: self,
field_data: None,
stored_fields,
reflect_type_data: None,
}
}
/// Converts the type information into a schema type information.
pub fn to_schema_type_info_with_metadata(
self,
metadata: &SchemaTypesMetadata,
) -> SchemaTypeInfo {
let stored_fields = (&self).into();
let reflect_type_data = if let Self::TypeRegistration(reg) = &self {
Some(metadata.get_registered_reflect_types(reg))
} else {
None
};
SchemaTypeInfo {
ty_info: self,
field_data: None,
stored_fields,
reflect_type_data,
}
}
/// Returns the documentation of the type.
@ -834,13 +876,14 @@ impl From<&TypeInformation> for InternalSchemaType {
if let Some(type_info) = value.try_get_type_info() {
match type_info {
TypeInfo::Struct(struct_info) => {
let fields = get_fields_information(struct_info.iter());
let fields_type = if value.is_forced_as_array() {
FieldType::Unnamed
FieldType::ForceUnnamed
} else {
FieldType::Named
};
InternalSchemaType::FieldsHolder(FieldsInformation {
fields: get_fields_information(struct_info.iter()),
fields,
fields_type,
})
}
@ -883,7 +926,7 @@ impl From<&TypeInformation> for InternalSchemaType {
key: (map_info.key_info(), &map_info.key_ty()).into(),
value: (map_info.value_info(), &map_info.value_ty()).into(),
},
TypeInfo::Opaque(t) => InternalSchemaType::Regular(t.type_id()),
TypeInfo::Opaque(t) => InternalSchemaType::RegularType(*t.ty()),
}
} else {
match value {
@ -925,18 +968,14 @@ impl From<&InternalSchemaType> for Option<SchemaTypeVariant> {
VariantInfo::Struct(_) => Some(SchemaTypeVariant::Single(SchemaType::Object)),
VariantInfo::Unit(_) => Some(SchemaTypeVariant::Single(SchemaType::String)),
},
InternalSchemaType::FieldsHolder(fields) => {
if fields.fields_type == FieldType::Unnamed {
if fields.fields.len() == 1 {
let schema: InternalSchemaType = (&fields.fields[0].type_info).into();
(&schema).into()
} else {
Some(SchemaTypeVariant::Single(SchemaType::Array))
}
} else {
Some(SchemaTypeVariant::Single(SchemaType::Object))
InternalSchemaType::FieldsHolder(fields) => match fields.fields_type {
FieldType::Named => Some(SchemaTypeVariant::Single(SchemaType::Object)),
FieldType::Unnamed if fields.fields.len() == 1 => {
let schema: InternalSchemaType = (&fields.fields[0].type_info).into();
(&schema).into()
}
}
_ => Some(SchemaTypeVariant::Single(SchemaType::Array)),
},
InternalSchemaType::Optional {
generic,
schema_type_info: _,
@ -972,23 +1011,11 @@ impl From<&FieldInformation> for SchemaTypeInfo {
ty_info: value.type_info.clone(),
field_data: Some(value.field.clone()),
stored_fields: (&value.type_info).into(),
reflect_type_data: None,
}
}
}
/// 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 SchemaTypeInfo {
/// Information about the type of the schema.
pub ty_info: TypeInformation,
/// Field information for the type.
pub field_data: Option<SchemaFieldData>,
/// Fields stored in the type.
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.
@ -1015,6 +1042,21 @@ impl From<SchemaDefinition> for JsonSchemaBevyType {
}
}
/// 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 SchemaTypeInfo {
/// Information about the type of the schema.
pub ty_info: TypeInformation,
/// Field information for the type.
pub field_data: Option<SchemaFieldData>,
/// Fields stored in the type.
pub stored_fields: Option<FieldsInformation>,
/// Bevy specific field, names of the types that type reflects. Mapping of the names to the data types is provided by [`SchemaTypesMetadata`].
pub reflect_type_data: Option<Vec<Cow<'static, str>>>,
}
impl SchemaTypeInfo {
/// Get the documentation for the schema type.
/// If the field has a description, it is returned.
@ -1052,7 +1094,12 @@ impl SchemaTypeInfo {
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());
let (ref_type, schema_type) = (
self.ty_info
.try_get_type_reference_id()
.map(TypeReferencePath::definition),
self.into(),
);
let mut schema = JsonSchemaBevyType {
description,
@ -1076,6 +1123,7 @@ impl SchemaTypeInfo {
ty_info: element_ty.clone(),
field_data: None,
stored_fields: None,
reflect_type_data: None,
};
schema.items = Some(items_schema.to_ref_schema().into());
schema.min_items = min_size;
@ -1084,10 +1132,6 @@ impl SchemaTypeInfo {
InternalSchemaType::EnumHolder(_)
| InternalSchemaType::EnumVariant(_)
| InternalSchemaType::FieldsHolder(_)
| InternalSchemaType::Optional {
generic: _,
schema_type_info: _,
}
| InternalSchemaType::Map { key: _, value: _ } => {
schema.ref_type = None;
}
@ -1099,9 +1143,15 @@ impl SchemaTypeInfo {
/// Converts the schema type information into a JSON schema definition.
pub fn to_definition(&self) -> SchemaDefinition {
let mut 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_reference_id();
let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new();
if let Some(custom_schema) = &self.ty_info.try_get_custom_schema() {
return SchemaDefinition {
id,
schema: custom_schema.0.clone(),
definitions,
};
}
let range = self.ty_info.get_range();
let (type_path, short_path, crate_name, module_path) =
@ -1115,7 +1165,12 @@ impl SchemaTypeInfo {
} else {
(Cow::default(), Cow::default(), None, None)
};
let schema_id = id
.as_ref()
.map(|id| Cow::Owned(format!("urn:bevy:{}", id)))
.unwrap_or_default();
let mut schema = JsonSchemaBevyType {
id: schema_id,
description: self.ty_info.get_docs(),
type_path,
short_path,
@ -1127,6 +1182,7 @@ impl SchemaTypeInfo {
exclusive_minimum: range.min.get_exclusive(),
exclusive_maximum: range.max.get_exclusive(),
schema_type: self.into(),
reflect_type_data: self.reflect_type_data.clone().unwrap_or_default(),
..default()
};
let internal_type: InternalSchemaType = (self).into();
@ -1136,11 +1192,13 @@ impl SchemaTypeInfo {
ty_info: key.clone(),
field_data: None,
stored_fields: None,
reflect_type_data: None,
};
let value: SchemaTypeInfo = SchemaTypeInfo {
ty_info: value.clone(),
field_data: None,
stored_fields: None,
reflect_type_data: None,
};
if !key.ty_info.is_primitive_type() {
let SchemaDefinition {
@ -1204,6 +1262,7 @@ impl SchemaTypeInfo {
fields,
fields_type: FieldType::Named,
}),
reflect_type_data: None,
};
let definition = schema_field.to_definition();
@ -1222,6 +1281,7 @@ impl SchemaTypeInfo {
fields_type: FieldType::Unnamed,
}),
reflect_type_data: None,
};
let definition = schema_field.to_definition();
@ -1235,6 +1295,7 @@ impl SchemaTypeInfo {
ty_info,
field_data,
stored_fields: None,
reflect_type_data: None,
};
return SchemaDefinition {
id: None,
@ -1272,11 +1333,11 @@ impl SchemaTypeInfo {
schema: _,
definitions: field_definitions,
} = field_schema.to_definition();
definitions.extend(field_definitions);
let Some(id) = id else { continue };
if !definitions.contains_key(&id) {
definitions.insert(id, field_schema);
}
definitions.extend(field_definitions);
}
schema.required = fields
.fields
@ -1284,45 +1345,70 @@ impl SchemaTypeInfo {
.map(|field| field.field.to_name())
.collect();
}
FieldType::Unnamed => {
if fields.fields.len() == 1 {
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
FieldType::Unnamed if fields.fields.len() == 1 => {
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
let SchemaDefinition {
id,
schema: new_schema_type,
definitions: field_definitions,
} = field_schema.to_definition();
definitions.extend(field_definitions);
if let Some(id) = id {
definitions.insert(id, field_schema);
}
schema = new_schema_type;
schema.schema_type = self.into();
schema.description = self.get_docs();
}
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();
for field_schema in schema_fields {
if field_schema.ty_info.is_primitive_type() {
continue;
}
let SchemaDefinition {
id,
schema: new_schema_type,
definitions: _,
schema: _,
definitions: field_definitions,
} = field_schema.to_definition();
if let Some(id) = id {
definitions.extend(field_definitions);
let Some(id) = id else { continue };
if !definitions.contains_key(&id) {
definitions.insert(id, field_schema);
}
schema = new_schema_type;
schema.schema_type = self.into();
schema.description = self.get_docs();
} else {
let schema_fields: Vec<SchemaTypeInfo> =
fields.fields.iter().map(SchemaTypeInfo::from).collect();
schema.prefix_items = schema_fields
.iter()
.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);
}
schema.min_items = Some(fields.fields.len() as u64);
schema.max_items = Some(fields.fields.len() as u64);
}
},
InternalSchemaType::Array {
@ -1335,6 +1421,7 @@ impl SchemaTypeInfo {
ty_info: element_ty.clone(),
field_data: None,
stored_fields: None,
reflect_type_data: None,
};
schema.items = Some(items_schema.to_ref_schema().into());
schema.min_items = min_size;
@ -1346,9 +1433,9 @@ impl SchemaTypeInfo {
schema: _,
definitions: field_definitions,
} = items_schema.to_definition();
definitions.extend(field_definitions);
if let Some(id) = id {
definitions.insert(id, items_schema);
definitions.extend(field_definitions);
}
}
}
@ -1360,16 +1447,7 @@ impl 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);
return schema_optional.to_definition();
}
}
SchemaDefinition {
@ -1479,6 +1557,7 @@ where
ty_info,
field_data: Some(field_data),
stored_fields,
reflect_type_data: None,
}
}
}
@ -1762,6 +1841,38 @@ mod tests {
let _: JsonSchemaBevyType = type_info.to_definition().into();
}
#[test]
fn reflect_multiple_definitions() {
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct BaseStruct {
pub base_field: i32,
pub second_field: i32,
}
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct ArrayComponent {
pub array: [BaseStruct; 3],
}
#[derive(Reflect, Default, Deserialize, Serialize)]
pub struct ArrayComponentWithMoreVariants {
pub array: [BaseStruct; 3],
pub list: Vec<BaseStruct>,
pub optional: Option<BaseStruct>,
}
let type_info = TypeInformation::from(&ArrayComponent::get_type_registration())
.to_schema_type_info()
.to_definition();
let type_info_second =
TypeInformation::from(&ArrayComponentWithMoreVariants::get_type_registration())
.to_schema_type_info()
.to_definition();
assert_eq!(
type_info.definitions.len(),
type_info_second.definitions.len()
);
// let schema: JsonSchemaBevyType = type_info_second.into();
// eprintln!("{}", serde_json::to_string_pretty(&schema).expect(""));
}
#[test]
fn reflect_struct_with_hashmap() {
#[derive(Reflect, Default, Deserialize, Serialize)]