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.
use core::any::TypeId;
use std::borrow::Cow;
use anyhow::{anyhow, Result as AnyhowResult};
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))
})
.collect::<HashMap<String, JsonSchemaBevyType>>();
.collect::<HashMap<Cow<'static, str>, JsonSchemaBevyType>>();
serde_json::to_value(schemas).map_err(BrpError::internal)
}

View File

@ -8,6 +8,7 @@ use bevy_reflect::{
use core::any::TypeId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
use crate::schemas::{
reflect_info::{SchemaInfoReflect, SchemaNumber},
@ -102,11 +103,11 @@ impl TryFrom<&TypeRegistration> for JsonSchemaBevyType {
/// Identifies the JSON Schema version used in the schema.
#[derive(Deserialize, Serialize, Debug, Reflect, PartialEq, Clone)]
pub struct SchemaMarker(String);
pub struct SchemaMarker(Cow<'static, str>);
impl Default for SchemaMarker {
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.
#[serde(rename = "$ref")]
#[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.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub short_path: String,
#[serde(skip_serializing_if = "str::is_empty", default)]
pub short_path: Cow<'static, str>,
/// Bevy specific field, full path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub type_path: String,
#[serde(skip_serializing_if = "str::is_empty", default)]
pub type_path: Cow<'static, str>,
/// Bevy specific field, path of the module that type is part of.
#[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.
#[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.
#[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.
pub kind: SchemaKind,
/// 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
/// against the corresponding schema.
#[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.
#[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.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub one_of: Vec<JsonSchemaVariant>,
@ -452,19 +453,24 @@ mod tests {
let schema = export_type::<Foo>();
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component"
);
assert!(
schema.reflect_types.contains(&"Resource".to_owned()),
schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should be a resource"
);
let _ = schema.properties.get("a").expect("Missing `a` field");
let _ = schema.properties.get("b").expect("Missing `b` field");
assert!(
schema.required.contains(&"a".to_owned()),
schema.required.contains(&Cow::Borrowed("a")),
"Field a should be required"
);
assert!(
schema.required.contains(&Cow::Borrowed("b")),
"Field b should be required"
);
}
#[test]
@ -482,11 +488,11 @@ mod tests {
}
let schema = export_type::<EnumComponent>();
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");
@ -506,11 +512,11 @@ mod tests {
}
let schema = export_type::<EnumComponent>();
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");
@ -601,7 +607,7 @@ mod tests {
.export_type_json_schema::<SomeType>(&SchemaTypesMetadata::default())
.expect("Failed to export schema");
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should not be a component"
);
assert!(
@ -621,11 +627,11 @@ mod tests {
let schema = export_type::<TupleStructType>();
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
schema.reflect_types.contains(&Cow::Borrowed("Component")),
"Should be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
!schema.reflect_types.contains(&Cow::Borrowed("Resource")),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");

View File

@ -10,6 +10,7 @@ use bevy_reflect::{
TypeRegistration,
};
use core::any::TypeId;
use std::borrow::Cow;
use crate::schemas::json_schema::JsonSchemaBevyType;
@ -23,7 +24,7 @@ pub mod reflect_info;
#[reflect(Resource)]
pub struct SchemaTypesMetadata {
/// 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
@ -62,12 +63,12 @@ impl Default for SchemaTypesMetadata {
impl SchemaTypesMetadata {
/// 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());
}
/// 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
.iter()
.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`
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)
}
/// 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
.get(&id)
.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_info
.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(),
short_path: self
.type_info
.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(),
crate_name: self
.type_info
.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
.type_info
.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()),
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()),
@ -396,9 +396,8 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
range: MinMaxValues::default(),
};
schema.properties =
[(variant_info.name().to_string(), schema_field.into())].into();
schema.required = vec![variant_info.name().to_string()];
schema.properties = [(variant_info.name().into(), schema_field.into())].into();
schema.required = vec![variant_info.name().into()];
}
VariantInfo::Tuple(tuple_variant_info) => {
schema.kind = SchemaKind::Value;
@ -414,9 +413,8 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
type_id: Some(tuple_variant_info.type_id()),
range: MinMaxValues::default(),
};
schema.properties =
[(variant_info.name().to_string(), schema_field.into())].into();
schema.required = vec![variant_info.name().to_string()];
schema.properties = [(variant_info.name().into(), schema_field.into())].into();
schema.required = vec![variant_info.name().into()];
}
VariantInfo::Unit(unit_variant_info) => {
return JsonSchemaVariant::const_value(
@ -430,11 +428,11 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
schema.properties = named_fields
.iter()
.map(|field| (field.name().to_string(), field.build_schema()))
.map(|field| (field.name().into(), field.build_schema()))
.collect();
schema.required = named_fields
.iter()
.map(|field| field.name().to_string())
.map(|field| field.name().into())
.collect();
}
InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => {