Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion crates/bevy_remote/src/builtin_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,7 @@ 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 components = world.components();
let types = types.read();
let schemas = types
.iter()
Expand All @@ -1447,7 +1448,7 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
return None;
}
}
let (id, schema) = export_type(type_reg, extra_info);
let (id, schema) = export_type(type_reg, extra_info, components);

if !filter.type_limit.with.is_empty()
&& !filter
Expand Down Expand Up @@ -1719,6 +1720,7 @@ mod tests {
}

use super::*;
use crate::schemas::json_schema::{ComponentMetadata, RelationshipKind};
use bevy_ecs::{
component::Component, event::Event, observer::On, resource::Resource, system::ResMut,
};
Expand Down Expand Up @@ -1784,6 +1786,67 @@ mod tests {
assert!(world.resource::<TestResult>().0);
}

#[test]
fn export_registry_types_with_reliationship() {
#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
#[require(bevy_ecs::name::Name)]
#[relationship(relationship_target = FollowedBy)]
struct Following(Entity);

#[derive(Component, Debug, Reflect)]
#[reflect(Component, Debug)]
#[relationship_target(relationship = Following)]
struct FollowedBy(Vec<Entity>);

let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<Following>();
register.register::<FollowedBy>();
}

let mut world = World::new();
world.init_resource::<crate::schemas::SchemaTypesMetadata>();
world.insert_resource(atr);
world.register_component::<Following>();
world.register_component::<FollowedBy>();

let params = BrpJsonSchemaQueryFilter::default();

let params_value = In(Some(
serde_json::to_value(params).expect("Failed to serialize"),
));
let result_value =
export_registry_types(params_value, &world).expect("Failed to export registry types");

let result: HashMap<String, JsonSchemaBevyType> =
parse(result_value).expect("Failed to parse exported registry types");

let actual_following = result
.get("bevy_remote::builtin_methods::tests::Following")
.expect("Missing Following type in result")
.component_info
.clone();
let expected_following = Some(ComponentMetadata {
mutable: false,
required_component_types: vec!["bevy_ecs::name::Name".to_owned()],
relationship_kind: Some(RelationshipKind::Relationship),
});
let actual_followed_by = result
.get("bevy_remote::builtin_methods::tests::FollowedBy")
.expect("Missing FollowedBy type in result")
.component_info
.clone();
let expected_followed_by = Some(ComponentMetadata {
mutable: true,
required_component_types: Vec::new(),
relationship_kind: Some(RelationshipKind::RelationshipTarget),
});
assert_eq!(actual_following, expected_following);
assert_eq!(actual_followed_by, expected_followed_by);
}

#[test]
fn serialization_tests() {
test_serialize_deserialize(BrpQueryRow {
Expand Down
104 changes: 92 additions & 12 deletions crates/bevy_remote/src/schemas/json_schema.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Module with JSON Schema type for Bevy Registry Types.
//! It tries to follow this standard: <https://json-schema.org/specification>
use alloc::borrow::Cow;
use bevy_ecs::component::Components;
use bevy_ecs::{component::ComponentInfo, relationship::RelationshipAccessor};
use bevy_platform::collections::HashMap;
use bevy_reflect::{
enums::VariantInfo, GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration,
Expand All @@ -18,14 +20,16 @@ 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>())
self.export_type_json_schema_for_id(extra_info, TypeId::of::<T>(), components)
}
/// Export type JSON Schema.
fn export_type_json_schema_for_id(
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
components: &Components,
) -> Option<JsonSchemaBevyType>;
}

Expand All @@ -34,23 +38,28 @@ impl TypeRegistrySchemaReader for TypeRegistry {
&self,
extra_info: &SchemaTypesMetadata,
type_id: TypeId,
components: &Components,
) -> Option<JsonSchemaBevyType> {
let type_reg = self.get(type_id)?;
Some((type_reg, extra_info).into())
Some((type_reg, extra_info, components).into())
}
}

/// Exports schema info for a given type
pub fn export_type(
reg: &TypeRegistration,
metadata: &SchemaTypesMetadata,
components: &Components,
) -> (Cow<'static, str>, JsonSchemaBevyType) {
(reg.type_info().type_path().into(), (reg, metadata).into())
(
reg.type_info().type_path().into(),
(reg, metadata, components).into(),
)
}

impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {
let (reg, metadata) = value;
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();

Expand All @@ -64,6 +73,31 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
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 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| match relationship {
RelationshipAccessor::Relationship { .. } => RelationshipKind::Relationship,
RelationshipAccessor::RelationshipTarget { .. } => {
RelationshipKind::RelationshipTarget
}
});
ComponentMetadata {
mutable,
required_component_types,
relationship_kind,
}
});
match t {
TypeInfo::Struct(info) => {
typed_schema.properties = info
Expand Down Expand Up @@ -223,6 +257,9 @@ pub struct JsonSchemaBevyType {
/// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties".
#[serde(skip_serializing_if = "Option::is_none", default)]
pub additional_properties: Option<bool>,
/// Additional metadata if this schema represents a component.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub component_info: Option<ComponentMetadata>,
/// Validation succeeds if, for each name that appears in both the instance and as a name
/// within this keyword's value, the child instance for that name successfully validates
/// against the corresponding schema.
Expand Down Expand Up @@ -312,6 +349,29 @@ pub enum SchemaType {
Null,
}

/// Component-specific metadata. Related to [`ComponentInfo`].
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct ComponentMetadata {
/// Whether component is mutable or not.
pub mutable: bool,
/// Type path of [required components](`bevy_ecs::component::RequiredComponent`).
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub required_component_types: Vec<String>,
/// Kind of relationship, if the component has one of the [relationship traits](`bevy_ecs::relationship::Relationship`).
#[serde(skip_serializing_if = "Option::is_none", default)]
pub relationship_kind: Option<RelationshipKind>,
}

/// Kind of relationship.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RelationshipKind {
/// The child kind of relationship.
Relationship,
/// The parent kind of relationship.
RelationshipTarget,
}

/// Helper trait for generating json schema reference
trait SchemaJsonReference {
/// Reference to another type in schema.
Expand Down Expand Up @@ -393,7 +453,11 @@ mod tests {
.get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);

assert!(
schema.reflect_types.contains(&"Resource".to_owned()),
Expand Down Expand Up @@ -434,7 +498,11 @@ mod tests {
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component"
Expand Down Expand Up @@ -469,7 +537,11 @@ mod tests {
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
"Should not be a component"
Expand Down Expand Up @@ -517,7 +589,7 @@ mod tests {
.get(TypeId::of::<EnumComponent>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &metadata);
let (_, schema) = export_type(&foo_registration, &metadata, &Components::default());
assert!(
!metadata.has_type_data::<ReflectComponent>(&schema.reflect_types),
"Should not be a component"
Expand Down Expand Up @@ -554,7 +626,11 @@ mod tests {
.get(TypeId::of::<TupleStructType>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
let (_, schema) = export_type(
&foo_registration,
&SchemaTypesMetadata::default(),
&Components::default(),
);
assert!(
schema.reflect_types.contains(&"Component".to_owned()),
"Should be a component"
Expand Down Expand Up @@ -585,7 +661,11 @@ mod tests {
.get(TypeId::of::<Foo>())
.expect("SHOULD BE REGISTERED")
.clone();
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
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",
Expand Down