Use Cow instead of String in JSON schema types

This commit is contained in:
Piotr Siuszko 2025-06-23 17:20:16 +02:00
parent 16f9c87bdd
commit e93680baa3
4 changed files with 50 additions and 40 deletions

View File

@ -1,6 +1,7 @@
//! Built-in verbs for the Bevy Remote Protocol. //! Built-in verbs for the Bevy Remote Protocol.
use core::any::TypeId; use core::any::TypeId;
use std::borrow::Cow;
use anyhow::{anyhow, Result as AnyhowResult}; use anyhow::{anyhow, Result as AnyhowResult};
use bevy_ecs::{ use bevy_ecs::{
@ -1268,7 +1269,7 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
} }
Some((type_reg.type_info().type_path().into(), schema)) Some((type_reg.type_info().type_path().into(), schema))
}) })
.collect::<HashMap<String, JsonSchemaBevyType>>(); .collect::<HashMap<Cow<'static, str>, JsonSchemaBevyType>>();
serde_json::to_value(schemas).map_err(BrpError::internal) serde_json::to_value(schemas).map_err(BrpError::internal)
} }

View File

@ -8,6 +8,7 @@ use bevy_reflect::{
use core::any::TypeId; use core::any::TypeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::borrow::Cow;
use crate::schemas::{ use crate::schemas::{
reflect_info::{SchemaInfoReflect, SchemaNumber}, reflect_info::{SchemaInfoReflect, SchemaNumber},
@ -102,11 +103,11 @@ impl TryFrom<&TypeRegistration> for JsonSchemaBevyType {
/// Identifies the JSON Schema version used in the schema. /// Identifies the JSON Schema version used in the schema.
#[derive(Deserialize, Serialize, Debug, Reflect, PartialEq, Clone)] #[derive(Deserialize, Serialize, Debug, Reflect, PartialEq, Clone)]
pub struct SchemaMarker(String); pub struct SchemaMarker(Cow<'static, str>);
impl Default for SchemaMarker { impl Default for SchemaMarker {
fn default() -> Self { fn default() -> Self {
Self("https://json-schema.org/draft/2020-12/schema".to_string()) Self("https://json-schema.org/draft/2020-12/schema".into())
} }
} }
@ -125,22 +126,22 @@ pub struct JsonSchemaBevyType {
/// This keyword is used to reference a statically identified schema. /// This keyword is used to reference a statically identified schema.
#[serde(rename = "$ref")] #[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub ref_type: Option<String>, pub ref_type: Option<Cow<'static, str>>,
/// Bevy specific field, short path of the type. /// Bevy specific field, short path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)] #[serde(skip_serializing_if = "str::is_empty", default)]
pub short_path: String, pub short_path: Cow<'static, str>,
/// Bevy specific field, full path of the type. /// Bevy specific field, full path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)] #[serde(skip_serializing_if = "str::is_empty", default)]
pub type_path: String, pub type_path: Cow<'static, str>,
/// Bevy specific field, path of the module that type is part of. /// Bevy specific field, path of the module that type is part of.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub module_path: Option<String>, pub module_path: Option<Cow<'static, str>>,
/// Bevy specific field, name of the crate that type is part of. /// Bevy specific field, name of the crate that type is part of.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub crate_name: Option<String>, pub crate_name: Option<Cow<'static, str>>,
/// Bevy specific field, names of the types that type reflects. /// Bevy specific field, names of the types that type reflects.
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
pub reflect_types: Vec<String>, pub reflect_types: Vec<Cow<'static, str>>,
/// Bevy specific field, [`TypeInfo`] type mapping. /// Bevy specific field, [`TypeInfo`] type mapping.
pub kind: SchemaKind, pub kind: SchemaKind,
/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].
@ -167,10 +168,10 @@ pub struct JsonSchemaBevyType {
/// within this keyword's value, the child instance for that name successfully validates /// within this keyword's value, the child instance for that name successfully validates
/// against the corresponding schema. /// against the corresponding schema.
#[serde(skip_serializing_if = "HashMap::is_empty", default)] #[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub properties: HashMap<String, JsonSchemaVariant>, pub properties: HashMap<Cow<'static, str>, JsonSchemaVariant>,
/// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance.
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
pub required: Vec<String>, pub required: Vec<Cow<'static, str>>,
/// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. /// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value.
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
pub one_of: Vec<JsonSchemaVariant>, pub one_of: Vec<JsonSchemaVariant>,
@ -452,19 +453,24 @@ mod tests {
let schema = export_type::<Foo>(); let schema = export_type::<Foo>();
assert!( assert!(
!schema.reflect_types.contains(&"Component".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component" "Should not be a component"
); );
assert!( assert!(
schema.reflect_types.contains(&"Resource".to_owned()), schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should be a resource" "Should be a resource"
); );
let _ = schema.properties.get("a").expect("Missing `a` field"); let _ = schema.properties.get("a").expect("Missing `a` field");
let _ = schema.properties.get("b").expect("Missing `b` field"); let _ = schema.properties.get("b").expect("Missing `b` field");
assert!( assert!(
schema.required.contains(&"a".to_owned()), schema.required.contains(&Cow::Borrowed("a")),
"Field a should be required" "Field a should be required"
); );
assert!(
schema.required.contains(&Cow::Borrowed("b")),
"Field b should be required"
);
} }
#[test] #[test]
@ -482,11 +488,11 @@ mod tests {
} }
let schema = export_type::<EnumComponent>(); let schema = export_type::<EnumComponent>();
assert!( assert!(
schema.reflect_types.contains(&"Component".to_owned()), schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should be a component" "Should be a component"
); );
assert!( assert!(
!schema.reflect_types.contains(&"Resource".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource" "Should not be a resource"
); );
assert!(schema.properties.is_empty(), "Should not have any field"); assert!(schema.properties.is_empty(), "Should not have any field");
@ -506,11 +512,11 @@ mod tests {
} }
let schema = export_type::<EnumComponent>(); let schema = export_type::<EnumComponent>();
assert!( assert!(
!schema.reflect_types.contains(&"Component".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component" "Should not be a component"
); );
assert!( assert!(
!schema.reflect_types.contains(&"Resource".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource" "Should not be a resource"
); );
assert!(schema.properties.is_empty(), "Should not have any field"); assert!(schema.properties.is_empty(), "Should not have any field");
@ -601,7 +607,7 @@ mod tests {
.export_type_json_schema::<SomeType>(&SchemaTypesMetadata::default()) .export_type_json_schema::<SomeType>(&SchemaTypesMetadata::default())
.expect("Failed to export schema"); .expect("Failed to export schema");
assert!( assert!(
!schema.reflect_types.contains(&"Component".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component" "Should not be a component"
); );
assert!( assert!(
@ -621,11 +627,11 @@ mod tests {
let schema = export_type::<TupleStructType>(); let schema = export_type::<TupleStructType>();
assert!( assert!(
schema.reflect_types.contains(&"Component".to_owned()), schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should be a component" "Should be a component"
); );
assert!( assert!(
!schema.reflect_types.contains(&"Resource".to_owned()), !schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource" "Should not be a resource"
); );
assert!(schema.properties.is_empty(), "Should not have any field"); assert!(schema.properties.is_empty(), "Should not have any field");

View File

@ -10,6 +10,7 @@ use bevy_reflect::{
TypeRegistration, TypeRegistration,
}; };
use core::any::TypeId; use core::any::TypeId;
use std::borrow::Cow;
use crate::schemas::json_schema::JsonSchemaBevyType; use crate::schemas::json_schema::JsonSchemaBevyType;
@ -23,7 +24,7 @@ pub mod reflect_info;
#[reflect(Resource)] #[reflect(Resource)]
pub struct SchemaTypesMetadata { pub struct SchemaTypesMetadata {
/// Type Data id mapping to human-readable type names. /// Type Data id mapping to human-readable type names.
pub type_data_map: HashMap<TypeId, String>, pub type_data_map: HashMap<TypeId, Cow<'static, str>>,
} }
/// Reflect-compatible custom JSON Schema for this type /// Reflect-compatible custom JSON Schema for this type
@ -62,12 +63,12 @@ impl Default for SchemaTypesMetadata {
impl SchemaTypesMetadata { impl SchemaTypesMetadata {
/// Map `TypeId` of `TypeData` to a human-readable type name /// Map `TypeId` of `TypeData` to a human-readable type name
pub fn map_type_data<T: TypeData>(&mut self, name: impl Into<String>) { pub fn map_type_data<T: TypeData>(&mut self, name: impl Into<Cow<'static, str>>) {
self.type_data_map.insert(TypeId::of::<T>(), name.into()); self.type_data_map.insert(TypeId::of::<T>(), name.into());
} }
/// Build reflect types list for a given type registration /// Build reflect types list for a given type registration
pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec<String> { pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec<Cow<'static, str>> {
self.type_data_map self.type_data_map
.iter() .iter()
.filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone())))
@ -75,12 +76,16 @@ impl SchemaTypesMetadata {
} }
/// Checks if slice contains a type name that matches the checked `TypeData` /// Checks if slice contains a type name that matches the checked `TypeData`
pub fn has_type_data<T: TypeData>(&self, types_string_slice: &[String]) -> bool { pub fn has_type_data<T: TypeData>(&self, types_string_slice: &[Cow<'static, str>]) -> bool {
self.has_type_data_by_id(TypeId::of::<T>(), types_string_slice) self.has_type_data_by_id(TypeId::of::<T>(), types_string_slice)
} }
/// Checks if slice contains a type name that matches the checked `TypeData` by id. /// Checks if slice contains a type name that matches the checked `TypeData` by id.
pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { pub fn has_type_data_by_id(
&self,
id: TypeId,
types_string_slice: &[Cow<'static, str>],
) -> bool {
self.type_data_map self.type_data_map
.get(&id) .get(&id)
.is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s)))

View File

@ -334,21 +334,21 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
type_path: self type_path: self
.type_info .type_info
.as_ref() .as_ref()
.and_then(|s| Some(s.type_path_table().path().to_owned())) .and_then(|s| Some(s.type_path_table().path().into()))
.unwrap_or_default(), .unwrap_or_default(),
short_path: self short_path: self
.type_info .type_info
.as_ref() .as_ref()
.and_then(|s| Some(s.type_path_table().short_path().to_owned())) .and_then(|s| Some(s.type_path_table().short_path().into()))
.unwrap_or_default(), .unwrap_or_default(),
crate_name: self crate_name: self
.type_info .type_info
.as_ref() .as_ref()
.and_then(|s| s.type_path_table().crate_name().map(str::to_owned)), .and_then(|s| s.type_path_table().crate_name().map(Into::into)),
module_path: self module_path: self
.type_info .type_info
.as_ref() .as_ref()
.and_then(|s| s.type_path_table().module_path().map(str::to_owned)), .and_then(|s| s.type_path_table().module_path().map(Into::into)),
minimum: self.range.min.as_ref().and_then(|r| r.get_inclusive()), minimum: self.range.min.as_ref().and_then(|r| r.get_inclusive()),
maximum: self.range.max.as_ref().and_then(|r| r.get_inclusive()), maximum: self.range.max.as_ref().and_then(|r| r.get_inclusive()),
exclusive_minimum: self.range.min.as_ref().and_then(|r| r.get_exclusive()), exclusive_minimum: self.range.min.as_ref().and_then(|r| r.get_exclusive()),
@ -396,9 +396,8 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
range: MinMaxValues::default(), range: MinMaxValues::default(),
}; };
schema.properties = schema.properties = [(variant_info.name().into(), schema_field.into())].into();
[(variant_info.name().to_string(), schema_field.into())].into(); schema.required = vec![variant_info.name().into()];
schema.required = vec![variant_info.name().to_string()];
} }
VariantInfo::Tuple(tuple_variant_info) => { VariantInfo::Tuple(tuple_variant_info) => {
schema.kind = SchemaKind::Value; schema.kind = SchemaKind::Value;
@ -414,9 +413,8 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
type_id: Some(tuple_variant_info.type_id()), type_id: Some(tuple_variant_info.type_id()),
range: MinMaxValues::default(), range: MinMaxValues::default(),
}; };
schema.properties = schema.properties = [(variant_info.name().into(), schema_field.into())].into();
[(variant_info.name().to_string(), schema_field.into())].into(); schema.required = vec![variant_info.name().into()];
schema.required = vec![variant_info.name().to_string()];
} }
VariantInfo::Unit(unit_variant_info) => { VariantInfo::Unit(unit_variant_info) => {
return JsonSchemaVariant::const_value( return JsonSchemaVariant::const_value(
@ -430,11 +428,11 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object)); schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
schema.properties = named_fields schema.properties = named_fields
.iter() .iter()
.map(|field| (field.name().to_string(), field.build_schema())) .map(|field| (field.name().into(), field.build_schema()))
.collect(); .collect();
schema.required = named_fields schema.required = named_fields
.iter() .iter()
.map(|field| field.name().to_string()) .map(|field| field.name().into())
.collect(); .collect();
} }
InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => { InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => {