use alloc::borrow::Cow;
use bevy_ecs::component::{Components, StorageType};
use bevy_ecs::{component::ComponentInfo, relationship::RelationshipAccessor};
use bevy_platform::collections::HashMap;
use bevy_reflect::{
enums::VariantInfo, GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration,
TypeRegistry,
};
use core::any::TypeId;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::schemas::SchemaTypesMetadata;
pub trait TypeRegistrySchemaReader {
fn export_type_json_schema<T: GetTypeRegistration + 'static>(
&self,
extra_info: &SchemaTypesMetadata,
components: &Components,
) -> Option<JsonSchemaBevyType> {
self.export_type_json_schema_for_id(extra_info, TypeId::of::<T>(), components)
}
fn export_type_json_schema_for_id(
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
components: &Components,
) -> Option<JsonSchemaBevyType>;
}
impl TypeRegistrySchemaReader for TypeRegistry {
fn export_type_json_schema_for_id(
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
components: &Components,
) -> Option<JsonSchemaBevyType> {
let type_reg = self.get(type_id)?;
Some((type_reg, extra_info, components).into())
}
}
pub fn export_type(
reg: &TypeRegistration,
metadata: &SchemaTypesMetadata,
components: &Components,
) -> (Cow<'static, str>, JsonSchemaBevyType) {
(
reg.type_info().type_path().into(),
(reg, metadata, components).into(),
)
}
impl From<(&TypeRegistration, &SchemaTypesMetadata, &Components)> for JsonSchemaBevyType {
fn from(value: (&TypeRegistration, &SchemaTypesMetadata, &Components)) -> Self {
let (reg, metadata, components) = value;
let t = reg.type_info();
let binding = t.type_path_table();
let short_path = binding.short_path();
let type_path = binding.path();
let mut typed_schema = JsonSchemaBevyType {
reflect_types: metadata.get_registered_reflect_types(reg),
short_path: short_path.to_owned(),
type_path: type_path.to_owned(),
crate_name: binding.crate_name().map(str::to_owned),
module_path: binding.module_path().map(str::to_owned),
..Default::default()
};
let component_info: Option<&ComponentInfo> = components
.get_valid_id(t.type_id())
.and_then(|component_id| components.get_info(component_id));
typed_schema.component_info = component_info.map(|info| {
let mutable = info.mutable();
let storage_type = info.storage_type().into();
let is_send_and_sync = info.is_send_and_sync();
let required_component_types = info
.required_components()
.iter_ids()
.flat_map(|component_id| components.get_info(component_id))
.map(|info: &ComponentInfo| info.name().to_string())
.collect::<Vec<_>>();
let relationship_kind = info
.relationship_accessor()
.map(|&relationship| relationship.into());
ComponentMetadata {
mutable,
storage_type,
is_send_and_sync,
required_component_types,
relationship_kind,
}
});
match t {
TypeInfo::Struct(info) => {
typed_schema.properties = info
.iter()
.map(|field| (field.name().to_owned(), field.ty().ref_type()))
.collect::<HashMap<_, _>>();
typed_schema.required = info
.iter()
.filter(|field| !field.type_path().starts_with("core::option::Option"))
.map(|f| f.name().to_owned())
.collect::<Vec<_>>();
typed_schema.additional_properties = Some(false);
typed_schema.schema_type = SchemaType::Object;
typed_schema.kind = SchemaKind::Struct;
}
TypeInfo::Enum(info) => {
typed_schema.kind = SchemaKind::Enum;
let simple = info
.iter()
.all(|variant| matches!(variant, VariantInfo::Unit(_)));
if simple {
typed_schema.schema_type = SchemaType::String;
typed_schema.one_of = info
.iter()
.map(|variant| match variant {
VariantInfo::Unit(v) => v.name().into(),
_ => unreachable!(),
})
.collect::<Vec<_>>();
} else {
typed_schema.schema_type = SchemaType::Object;
typed_schema.one_of = info
.iter()
.map(|variant| match variant {
VariantInfo::Struct(v) => json!({
"type": "object",
"kind": "Struct",
"typePath": format!("{}::{}", type_path, v.name()),
"shortPath": v.name(),
"properties": v
.iter()
.map(|field| (field.name().to_owned(), field.ref_type()))
.collect::<Map<_, _>>(),
"additionalProperties": false,
"required": v
.iter()
.filter(|field| !field.type_path().starts_with("core::option::Option"))
.map(NamedField::name)
.collect::<Vec<_>>(),
}),
VariantInfo::Tuple(v) => json!({
"type": "array",
"kind": "Tuple",
"typePath": format!("{}::{}", type_path, v.name()),
"shortPath": v.name(),
"prefixItems": v
.iter()
.map(SchemaJsonReference::ref_type)
.collect::<Vec<_>>(),
"items": false,
}),
VariantInfo::Unit(v) => json!({
"typePath": format!("{}::{}", type_path, v.name()),
"shortPath": v.name(),
}),
})
.collect::<Vec<_>>();
}
}
TypeInfo::TupleStruct(info) => {
typed_schema.schema_type = SchemaType::Array;
typed_schema.kind = SchemaKind::TupleStruct;
typed_schema.prefix_items = info
.iter()
.map(SchemaJsonReference::ref_type)
.collect::<Vec<_>>();
typed_schema.items = Some(false.into());
}
TypeInfo::List(info) => {
typed_schema.schema_type = SchemaType::Array;
typed_schema.kind = SchemaKind::List;
typed_schema.items = info.item_ty().ref_type().into();
}
TypeInfo::Array(info) => {
typed_schema.schema_type = SchemaType::Array;
typed_schema.kind = SchemaKind::Array;
typed_schema.items = info.item_ty().ref_type().into();
}
TypeInfo::Map(info) => {
typed_schema.schema_type = SchemaType::Object;
typed_schema.kind = SchemaKind::Map;
typed_schema.key_type = info.key_ty().ref_type().into();
typed_schema.value_type = info.value_ty().ref_type().into();
}
TypeInfo::Tuple(info) => {
typed_schema.schema_type = SchemaType::Array;
typed_schema.kind = SchemaKind::Tuple;
typed_schema.prefix_items = info
.iter()
.map(SchemaJsonReference::ref_type)
.collect::<Vec<_>>();
typed_schema.items = Some(false.into());
}
TypeInfo::Set(info) => {
typed_schema.schema_type = SchemaType::Set;
typed_schema.kind = SchemaKind::Set;
typed_schema.items = info.value_ty().ref_type().into();
}
TypeInfo::Opaque(info) => {
typed_schema.schema_type = info.map_json_type();
typed_schema.kind = SchemaKind::Value;
}
};
typed_schema
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct JsonSchemaBevyType {
pub short_path: String,
pub type_path: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub module_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub crate_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub reflect_types: Vec<String>,
pub kind: SchemaKind,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub key_type: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub value_type: Option<Value>,
#[serde(rename = "type")]
pub schema_type: SchemaType,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub additional_properties: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub component_info: Option<ComponentMetadata>,
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub properties: HashMap<String, Value>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub required: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub one_of: Vec<Value>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub prefix_items: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub items: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub enum SchemaKind {
#[default]
Struct,
Enum,
Map,
Array,
List,
Tuple,
TupleStruct,
Set,
Value,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum SchemaType {
String,
Float,
Uint,
Int,
Object,
Array,
Boolean,
Set,
#[default]
Null,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct ComponentMetadata {
pub mutable: bool,
pub storage_type: StorageKind,
pub is_send_and_sync: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub required_component_types: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub relationship_kind: Option<RelationshipKind>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub enum StorageKind {
#[default]
Table,
SparseSet,
}
impl From<StorageType> for StorageKind {
fn from(value: StorageType) -> Self {
match value {
StorageType::Table => StorageKind::Table,
StorageType::SparseSet => StorageKind::SparseSet,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RelationshipKind {
Relationship,
RelationshipTarget,
}
impl From<RelationshipAccessor> for RelationshipKind {
fn from(value: RelationshipAccessor) -> Self {
match value {
RelationshipAccessor::Relationship { .. } => RelationshipKind::Relationship,
RelationshipAccessor::RelationshipTarget { .. } => RelationshipKind::RelationshipTarget,
}
}
}
trait SchemaJsonReference {
fn ref_type(self) -> Value;
}
pub trait SchemaJsonType {
fn get_type_path(&self) -> &'static str;
fn map_json_type(&self) -> SchemaType {
match self.get_type_path() {
"bool" => SchemaType::Boolean,
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint,
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int,
"f32" | "f64" => SchemaType::Float,
"char" | "str" | "alloc::string::String" => SchemaType::String,
_ => SchemaType::Object,
}
}
}
impl SchemaJsonType for OpaqueInfo {
fn get_type_path(&self) -> &'static str {
self.type_path()
}
}
impl SchemaJsonReference for &bevy_reflect::Type {
fn ref_type(self) -> Value {
let path = self.path();
json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})
}
}
impl SchemaJsonReference for &bevy_reflect::UnnamedField {
fn ref_type(self) -> Value {
let path = self.type_path();
json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})
}
}
impl SchemaJsonReference for &NamedField {
fn ref_type(self) -> Value {
let type_path = self.type_path();
json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()})
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_ecs::prelude::ReflectComponent;
use bevy_ecs::prelude::ReflectResource;
use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};
use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
#[test]
fn reflect_export_struct() {
#[derive(Reflect, Resource, Default, Deserialize, Serialize)]
#[reflect(Resource, Default, Serialize, Deserialize)]
struct Foo {
a: f32,
b: Option<f32>,
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<Foo>();
}
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
schema.reflect_types.contains(&"Resource".to_owned()),
"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()),
"Field a should be required"
);
assert!(
!schema.required.contains(&"b".to_owned()),
"Field b should not be required"
);
}
#[test]
fn reflect_export_enum() {
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
#[reflect(Component, Default, Serialize, Deserialize)]
enum EnumComponent {
ValueOne(i32),
ValueTwo {
test: i32,
},
#[default]
NoValue,
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<EnumComponent>();
}
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
}
#[test]
fn reflect_export_struct_without_reflect_types() {
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
enum EnumComponent {
ValueOne(i32),
ValueTwo {
test: i32,
},
#[default]
NoValue,
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<EnumComponent>();
}
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
"Should not be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
}
#[test]
fn reflect_struct_with_custom_type_data() {
#[derive(Reflect, Default, Deserialize, Serialize)]
#[reflect(Default)]
enum EnumComponent {
ValueOne(i32),
ValueTwo {
test: i32,
},
#[default]
NoValue,
}
#[derive(Clone)]
pub struct ReflectCustomData;
impl<T: Reflect> bevy_reflect::FromType<T> for ReflectCustomData {
fn from_type() -> Self {
ReflectCustomData
}
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<EnumComponent>();
register.register_type_data::<EnumComponent, ReflectCustomData>();
}
let mut metadata = SchemaTypesMetadata::default();
metadata.map_type_data::<ReflectCustomData>("CustomData");
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &metadata, &Components::default());
assert!(
!metadata.has_type_data::<ReflectComponent>(&schema.reflect_types),
"Should not be a component"
);
assert!(
!metadata.has_type_data::<ReflectResource>(&schema.reflect_types),
"Should not be a resource"
);
assert!(
metadata.has_type_data::<ReflectDefault>(&schema.reflect_types),
"Should have default"
);
assert!(
metadata.has_type_data::<ReflectCustomData>(&schema.reflect_types),
"Should have CustomData"
);
assert!(schema.properties.is_empty(), "Should not have any field");
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
}
#[test]
fn reflect_export_tuple_struct() {
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
#[reflect(Component, Default, Serialize, Deserialize)]
struct TupleStructType(usize, i32);
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<TupleStructType>();
}
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<TupleStructType>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component"
);
assert!(
!schema.reflect_types.contains(&"Resource".to_owned()),
"Should not be a resource"
);
assert!(schema.properties.is_empty(), "Should not have any field");
assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items");
}
#[test]
fn reflect_export_serialization_check() {
#[derive(Reflect, Resource, Default, Deserialize, Serialize)]
#[reflect(Resource, Default)]
struct Foo {
a: f32,
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<Foo>();
}
let type_registry = atr.read();
let foo_registration = type_registry
.get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
let schema_as_value = serde_json::to_value(&schema).expect("Should serialize");
let value = json!({
"shortPath": "Foo",
"typePath": "bevy_remote::schemas::json_schema::tests::Foo",
"modulePath": "bevy_remote::schemas::json_schema::tests",
"crateName": "bevy_remote",
"reflectTypes": [
"Component",
"Resource",
"Default",
],
"kind": "Struct",
"type": "object",
"additionalProperties": false,
"properties": {
"a": {
"type": {
"$ref": "#/$defs/f32"
}
},
},
"required": [
"a"
]
});
assert_normalized_values(schema_as_value, value);
}
fn assert_normalized_values(mut one: Value, mut two: Value) {
normalize_json(&mut one);
normalize_json(&mut two);
assert_eq!(one, two);
fn normalize_json(value: &mut Value) {
match value {
Value::Array(arr) => {
for v in arr.iter_mut() {
normalize_json(v);
}
arr.sort_by_key(ToString::to_string); }
Value::Object(map) => {
for (_k, v) in map.iter_mut() {
normalize_json(v);
}
}
_ => {}
}
}
}
}