From 4fc9009d7006b458aae2d94e34eb1670e2dfe1be Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 27 Feb 2025 10:58:49 +0100 Subject: [PATCH 1/8] RPC Discover endpoint with basic informations --- crates/bevy_remote/src/builtin_methods.rs | 577 ++---------------- crates/bevy_remote/src/lib.rs | 10 + crates/bevy_remote/src/schemas/json_schema.rs | 545 +++++++++++++++++ crates/bevy_remote/src/schemas/mod.rs | 4 + crates/bevy_remote/src/schemas/open_rpc.rs | 100 +++ 5 files changed, 706 insertions(+), 530 deletions(-) create mode 100644 crates/bevy_remote/src/schemas/json_schema.rs create mode 100644 crates/bevy_remote/src/schemas/mod.rs create mode 100644 crates/bevy_remote/src/schemas/open_rpc.rs diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index f75e4d4da363c..2efd0b18ac076 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -16,15 +16,21 @@ use bevy_ecs::{ }; use bevy_platform_support::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, serde::{ReflectSerializer, TypedReflectDeserializer}, - GetPath, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, TypeRegistry, VariantInfo, + GetPath, PartialReflect, TypeRegistration, TypeRegistry, }; +use bevy_utils::default; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; -use serde_json::{json, Map, Value}; +use serde_json::{Map, Value}; -use crate::{error_codes, BrpError, BrpResult}; +use crate::{ + error_codes, + schemas::{ + json_schema::JsonSchemaBevyType, + open_rpc::{OpenRpcDocument, ServerObject}, + }, + BrpError, BrpResult, +}; /// The method path for a `bevy/get` request. pub const BRP_GET_METHOD: &str = "bevy/get"; @@ -77,6 +83,9 @@ pub const BRP_LIST_RESOURCES_METHOD: &str = "bevy/list_resources"; /// The method path for a `bevy/registry/schema` request. pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "bevy/registry/schema"; +/// The method path for a `rpc.discover` request. +pub const RPC_DISCOVER_METHOD: &str = "rpc.discover"; + /// `bevy/get`: Retrieves one or more components from the entity with the given /// ID. /// @@ -806,6 +815,38 @@ pub fn process_remote_spawn_request(In(params): In>, world: &mut W serde_json::to_value(response).map_err(BrpError::internal) } +/// Handles a `rpc.discover` request coming from a client. +pub fn process_remote_list_methods_request( + In(_params): In>, + world: &mut World, +) -> BrpResult { + let remote_methods = world.resource::(); + let servers = match ( + world.get_resource::(), + world.get_resource::(), + ) { + (Some(url), Some(port)) => Some(vec![ServerObject { + name: "Server".to_owned(), + url: format!("{}:{}", url.0, port.0), + ..default() + }]), + (Some(url), None) => Some(vec![ServerObject { + name: "Server".to_owned(), + url: url.0.to_string(), + ..default() + }]), + _ => None, + }; + let doc = OpenRpcDocument { + info: Default::default(), + methods: remote_methods.into(), + openrpc: "1.0.0-rc1".to_owned(), + servers, + }; + + serde_json::to_value(doc).map_err(BrpError::internal) +} + /// Handles a `bevy/insert` request (insert components) coming from a client. pub fn process_remote_insert_request( In(params): In>, @@ -1162,7 +1203,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br let types = types.read(); let schemas = types .iter() - .map(export_type) + .map(crate::schemas::json_schema::export_type) .filter(|(_, schema)| { if let Some(crate_name) = &schema.crate_name { if !filter.with_crates.is_empty() @@ -1202,339 +1243,6 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br serde_json::to_value(schemas).map_err(BrpError::internal) } -/// Exports schema info for a given type -fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - 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: 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() - }; - match t { - TypeInfo::Struct(info) => { - typed_schema.properties = info - .iter() - .map(|field| (field.name().to_owned(), field.ty().ref_type())) - .collect::>(); - typed_schema.required = info - .iter() - .filter(|field| !field.type_path().starts_with("core::option::Option")) - .map(|f| f.name().to_owned()) - .collect::>(); - 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::>(); - } 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::>(), - "additionalProperties": false, - "required": v - .iter() - .filter(|field| !field.type_path().starts_with("core::option::Option")) - .map(NamedField::name) - .collect::>(), - }), - 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::>(), - "items": false, - }), - VariantInfo::Unit(v) => json!({ - "typePath": format!("{}::{}", type_path, v.name()), - "shortPath": v.name(), - }), - }) - .collect::>(); - } - } - 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::>(); - 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::>(); - 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; - } - }; - - (t.type_path().to_owned(), typed_schema) -} - -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } - } - result -} - -/// JSON Schema type for Bevy Registry Types -/// It tries to follow this standard: -/// -/// To take the full advantage from info provided by Bevy registry it provides extra fields -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "camelCase")] -pub struct JsonSchemaBevyType { - /// Bevy specific field, short path of the type. - pub short_path: String, - /// Bevy specific field, full path of the type. - pub type_path: String, - /// Bevy specific field, path of the module that type is part of. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub module_path: Option, - /// Bevy specific field, name of the crate that type is part of. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub crate_name: Option, - /// Bevy specific field, names of the types that type reflects. - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub reflect_types: Vec, - /// Bevy specific field, [`TypeInfo`] type mapping. - pub kind: SchemaKind, - /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. - /// - /// It contains type info of key of the Map. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub key_type: Option, - /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. - /// - /// It contains type info of value of the Map. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub value_type: Option, - /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. - #[serde(rename = "type")] - pub schema_type: SchemaType, - /// The behavior of this keyword depends on the presence and annotation results of "properties" - /// and "patternProperties" within the same schema object. - /// Validation with "additionalProperties" applies only to the child - /// 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, - /// 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. - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - pub properties: HashMap, - /// 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, - /// 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, - /// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. - /// - /// This keyword produces an annotation value which is the largest index to which this keyword - /// applied a subschema. The value MAY be a boolean true if a subschema was applied to every - /// index of the instance, such as is produced by the "items" keyword. - /// This annotation affects the behavior of "items" and "unevaluatedItems". - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub prefix_items: Vec, - /// This keyword applies its subschema to all instance elements at indexes greater - /// than the length of the "prefixItems" array in the same schema object, - /// as reported by the annotation result of that "prefixItems" keyword. - /// If no such annotation result exists, "items" applies its subschema to all - /// instance array elements. - /// - /// If the "items" subschema is applied to any positions within the instance array, - /// it produces an annotation result of boolean true, indicating that all remaining - /// array elements have been evaluated against this keyword's subschema. - #[serde(skip_serializing_if = "Option::is_none", default)] - pub items: Option, -} - -/// Kind of json schema, maps [`TypeInfo`] type -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -pub enum SchemaKind { - /// Struct - #[default] - Struct, - /// Enum type - Enum, - /// A key-value map - Map, - /// Array - Array, - /// List - List, - /// Fixed size collection of items - Tuple, - /// Fixed size collection of items with named fields - TupleStruct, - /// Set of unique values - Set, - /// Single value, eg. primitive types - Value, -} - -/// Type of json schema -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -#[serde(rename_all = "lowercase")] -pub enum SchemaType { - /// Represents a string value. - String, - - /// Represents a floating-point number. - Float, - - /// Represents an unsigned integer. - Uint, - - /// Represents a signed integer. - Int, - - /// Represents an object with key-value pairs. - Object, - - /// Represents an array of values. - Array, - - /// Represents a boolean value (true or false). - Boolean, - - /// Represents a set of unique values. - Set, - - /// Represents a null value. - #[default] - Null, -} - -/// Helper trait for generating json schema reference -trait SchemaJsonReference { - /// Reference to another type in schema. - /// The value `$ref` is a URI-reference that is resolved against the schema. - fn ref_type(self) -> Value; -} - -/// Helper trait for mapping bevy type path into json schema type -trait SchemaJsonType { - /// Bevy Reflect type path - fn get_type_path(&self) -> &'static str; - - /// JSON Schema type keyword from Bevy reflect type path into - 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()}) - } -} - /// Immutably retrieves an entity from the [`World`], returning an error if the /// entity isn't present. fn get_entity(world: &World, entity: Entity) -> Result, BrpError> { @@ -1778,8 +1486,6 @@ mod tests { ); } use super::*; - use bevy_ecs::{component::Component, resource::Resource}; - use bevy_reflect::Reflect; #[test] fn serialization_tests() { @@ -1802,193 +1508,4 @@ mod tests { entity: Entity::from_raw(0), }); } - - #[test] - fn reflect_export_struct() { - #[derive(Reflect, Resource, Default, Deserialize, Serialize)] - #[reflect(Resource, Default, Serialize, Deserialize)] - struct Foo { - a: f32, - b: Option, - } - - let atr = AppTypeRegistry::default(); - { - let mut register = atr.write(); - register.register::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); - - assert!( - !schema.reflect_types.contains(&"Component".to_owned()), - "Should not be a component" - ); - 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::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - 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::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - 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_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::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); - 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::(); - } - let type_registry = atr.read(); - let foo_registration = type_registry - .get(TypeId::of::()) - .expect("SHOULD BE REGISTERED") - .clone(); - let (_, schema) = export_type(&foo_registration); - let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); - let value = json!({ - "shortPath": "Foo", - "typePath": "bevy_remote::builtin_methods::tests::Foo", - "modulePath": "bevy_remote::builtin_methods::tests", - "crateName": "bevy_remote", - "reflectTypes": [ - "Resource", - "Default", - ], - "kind": "Struct", - "type": "object", - "additionalProperties": false, - "properties": { - "a": { - "type": { - "$ref": "#/$defs/f32" - } - }, - }, - "required": [ - "a" - ] - }); - assert_eq!(schema_as_value, value); - } } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 39e1b8c5268fa..2e05019227c5e 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -383,6 +383,7 @@ use std::sync::RwLock; pub mod builtin_methods; #[cfg(feature = "http")] pub mod http; +pub mod schemas; const CHANNEL_SIZE: usize = 16; @@ -474,6 +475,10 @@ impl Default for RemotePlugin { builtin_methods::BRP_MUTATE_COMPONENT_METHOD, builtin_methods::process_remote_mutate_component_request, ) + .with_method( + builtin_methods::RPC_DISCOVER_METHOD, + builtin_methods::process_remote_list_methods_request, + ) .with_watching_method( builtin_methods::BRP_GET_AND_WATCH_METHOD, builtin_methods::process_remote_get_watching_request, @@ -631,6 +636,11 @@ impl RemoteMethods { pub fn get(&self, method: &str) -> Option<&RemoteMethodSystemId> { self.0.get(method) } + + /// Get a [`Vec`] with method names + pub fn methods(&self) -> Vec { + self.0.keys().into_iter().cloned().collect() + } } /// Holds the [`BrpMessage`]'s of all ongoing watching requests along with their handlers. diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs new file mode 100644 index 0000000000000..14b09a0004b56 --- /dev/null +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -0,0 +1,545 @@ +//! Module with JSON Schema type for Bevy Registry Types. +//! It tries to follow this standard: +use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use bevy_platform_support::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, + TypeInfo, TypeRegistration, VariantInfo, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Value}; +use std::any::TypeId; + +/// Exports schema info for a given type +pub(crate) fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { + (reg.type_info().type_path().to_owned(), reg.into()) +} + +fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { + // Vec could be moved to allow registering more types by game maker. + let registered_reflect_types: [(TypeId, &str); 5] = [ + { (TypeId::of::(), "Component") }, + { (TypeId::of::(), "Resource") }, + { (TypeId::of::(), "Default") }, + { (TypeId::of::(), "Serialize") }, + { (TypeId::of::(), "Deserialize") }, + ]; + let mut result = Vec::new(); + for (id, name) in registered_reflect_types { + if reg.data_by_id(id).is_some() { + result.push(name.to_owned()); + } + } + result +} + +impl From<&TypeRegistration> for JsonSchemaBevyType { + fn from(reg: &TypeRegistration) -> Self { + 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: 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() + }; + match t { + TypeInfo::Struct(info) => { + typed_schema.properties = info + .iter() + .map(|field| (field.name().to_owned(), field.ty().ref_type())) + .collect::>(); + typed_schema.required = info + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(|f| f.name().to_owned()) + .collect::>(); + 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::>(); + } 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::>(), + "additionalProperties": false, + "required": v + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(NamedField::name) + .collect::>(), + }), + 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::>(), + "items": false, + }), + VariantInfo::Unit(v) => json!({ + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + }), + }) + .collect::>(); + } + } + 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::>(); + 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::>(); + 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 + } +} + +/// JSON Schema type for Bevy Registry Types +/// It tries to follow this standard: +/// +/// To take the full advantage from info provided by Bevy registry it provides extra fields +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct JsonSchemaBevyType { + /// Bevy specific field, short path of the type. + pub short_path: String, + /// Bevy specific field, full path of the type. + pub type_path: String, + /// Bevy specific field, path of the module that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub module_path: Option, + /// Bevy specific field, name of the crate that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub crate_name: Option, + /// Bevy specific field, names of the types that type reflects. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub reflect_types: Vec, + /// Bevy specific field, [`TypeInfo`] type mapping. + pub kind: SchemaKind, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of key of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub key_type: Option, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of value of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub value_type: Option, + /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. + #[serde(rename = "type")] + pub schema_type: SchemaType, + /// The behavior of this keyword depends on the presence and annotation results of "properties" + /// and "patternProperties" within the same schema object. + /// Validation with "additionalProperties" applies only to the child + /// 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, + /// 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. + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + pub properties: HashMap, + /// 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, + /// 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, + /// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. + /// + /// This keyword produces an annotation value which is the largest index to which this keyword + /// applied a subschema. The value MAY be a boolean true if a subschema was applied to every + /// index of the instance, such as is produced by the "items" keyword. + /// This annotation affects the behavior of "items" and "unevaluatedItems". + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub prefix_items: Vec, + /// This keyword applies its subschema to all instance elements at indexes greater + /// than the length of the "prefixItems" array in the same schema object, + /// as reported by the annotation result of that "prefixItems" keyword. + /// If no such annotation result exists, "items" applies its subschema to all + /// instance array elements. + /// + /// If the "items" subschema is applied to any positions within the instance array, + /// it produces an annotation result of boolean true, indicating that all remaining + /// array elements have been evaluated against this keyword's subschema. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub items: Option, +} + +/// Kind of json schema, maps [`TypeInfo`] type +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub enum SchemaKind { + /// Struct + #[default] + Struct, + /// Enum type + Enum, + /// A key-value map + Map, + /// Array + Array, + /// List + List, + /// Fixed size collection of items + Tuple, + /// Fixed size collection of items with named fields + TupleStruct, + /// Set of unique values + Set, + /// Single value, eg. primitive types + Value, +} + +/// Type of json schema +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum SchemaType { + /// Represents a string value. + String, + + /// Represents a floating-point number. + Float, + + /// Represents an unsigned integer. + Uint, + + /// Represents a signed integer. + Int, + + /// Represents an object with key-value pairs. + Object, + + /// Represents an array of values. + Array, + + /// Represents a boolean value (true or false). + Boolean, + + /// Represents a set of unique values. + Set, + + /// Represents a null value. + #[default] + Null, +} + +/// Helper trait for generating json schema reference +trait SchemaJsonReference { + /// Reference to another type in schema. + /// The value `$ref` is a URI-reference that is resolved against the schema. + fn ref_type(self) -> Value; +} + +/// Helper trait for mapping bevy type path into json schema type +pub trait SchemaJsonType { + /// Bevy Reflect type path + fn get_type_path(&self) -> &'static str; + + /// JSON Schema type keyword from Bevy reflect type path into + 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::{component::Component, reflect::AppTypeRegistry, resource::Resource}; + use bevy_reflect::Reflect; + + #[test] + fn reflect_export_struct() { + #[derive(Reflect, Resource, Default, Deserialize, Serialize)] + #[reflect(Resource, Default, Serialize, Deserialize)] + struct Foo { + a: f32, + b: Option, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); + + assert!( + !schema.reflect_types.contains(&"Component".to_owned()), + "Should not be a component" + ); + 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::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + 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::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + 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_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::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); + 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::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); + let value = json!({ + "shortPath": "Foo", + "typePath": "bevy_remote::builtin_methods::tests::Foo", + "modulePath": "bevy_remote::builtin_methods::tests", + "crateName": "bevy_remote", + "reflectTypes": [ + "Resource", + "Default", + ], + "kind": "Struct", + "type": "object", + "additionalProperties": false, + "properties": { + "a": { + "type": { + "$ref": "#/$defs/f32" + } + }, + }, + "required": [ + "a" + ] + }); + assert_eq!(schema_as_value, value); + } +} diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs new file mode 100644 index 0000000000000..7104fd5547549 --- /dev/null +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -0,0 +1,4 @@ +//! Module with schemas used for various BRP endpoints + +pub mod json_schema; +pub mod open_rpc; diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs new file mode 100644 index 0000000000000..a43a69f06d479 --- /dev/null +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -0,0 +1,100 @@ +//! Module with trimmed down OpenRPC document structs. +//! It tries to follow this standard: +use bevy_platform_support::collections::HashMap; +use bevy_utils::default; +use serde::{Deserialize, Serialize}; + +use crate::RemoteMethods; + +/// Represents an OpenRPC document as defined by the OpenRPC specification. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OpenRpcDocument { + /// The version of the OpenRPC specification being used. + pub openrpc: String, + /// Informational metadata about the document. + pub info: InfoObject, + /// List of RPC methods defined in the document. + pub methods: Vec, + /// Optional list of server objects that provide the API endpoint details. + pub servers: Option>, +} + +/// Contains metadata information about the OpenRPC document. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InfoObject { + /// The title of the API or document. + pub title: String, + /// The version of the API. + pub version: String, + /// An optional description providing additional details about the API. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A collection of custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +impl Default for InfoObject { + fn default() -> Self { + Self { + title: "Bevy OpenRPC".to_owned(), + version: env!("CARGO_PKG_VERSION").to_owned(), + description: None, + extensions: Default::default(), + } + } +} + +/// Describes a server hosting the API as specified in the OpenRPC document. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct ServerObject { + /// The name of the server. + pub name: String, + /// The URL endpoint of the server. + pub url: String, + /// An optional description of the server. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +/// Represents an RPC method in the OpenRPC document. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct MethodObject { + /// The method name (e.g., "/bevy/get") + pub name: String, + /// An optional short summary of the method. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, + /// An optional detailed description of the method. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + // /// Parameters for the RPC method + // #[serde(default)] + // pub params: Vec, + // /// The expected result of the method + // #[serde(skip_serializing_if = "Option::is_none")] + // pub result: Option, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + +impl From<&RemoteMethods> for Vec { + fn from(value: &RemoteMethods) -> Self { + value + .methods() + .iter() + .map(|e| MethodObject { + name: e.to_owned(), + ..default() + }) + .collect() + } +} From 40c4753ea5dd35be120b47eab8a0b7dbfb6d32fb Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 27 Feb 2025 11:18:16 +0100 Subject: [PATCH 2/8] Test fix --- crates/bevy_remote/src/schemas/json_schema.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 14b09a0004b56..fa44d2f3b58cc 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -11,7 +11,7 @@ use serde_json::{json, Map, Value}; use std::any::TypeId; /// Exports schema info for a given type -pub(crate) fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { +pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { (reg.type_info().type_path().to_owned(), reg.into()) } @@ -519,8 +519,8 @@ mod tests { let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", - "typePath": "bevy_remote::builtin_methods::tests::Foo", - "modulePath": "bevy_remote::builtin_methods::tests", + "typePath": "bevy_remote::schemas::json_schema::tests::Foo", + "modulePath": "bevy_remote::schemas::json_schema::tests", "crateName": "bevy_remote", "reflectTypes": [ "Resource", From 3aa1d41353201efce1633de2c7d9ceadbedd5bbb Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Thu, 27 Feb 2025 11:27:31 +0100 Subject: [PATCH 3/8] Docs fixes --- crates/bevy_remote/src/lib.rs | 4 ++-- crates/bevy_remote/src/schemas/json_schema.rs | 2 +- crates/bevy_remote/src/schemas/open_rpc.rs | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 2e05019227c5e..2fdb08720c14d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -637,9 +637,9 @@ impl RemoteMethods { self.0.get(method) } - /// Get a [`Vec`] with method names + /// Get a [`Vec`] with method names. pub fn methods(&self) -> Vec { - self.0.keys().into_iter().cloned().collect() + self.0.keys().cloned().collect() } } diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index fa44d2f3b58cc..cf0d58dea96a7 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -6,9 +6,9 @@ use bevy_reflect::{ prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, TypeInfo, TypeRegistration, VariantInfo, }; +use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -use std::any::TypeId; /// Exports schema info for a given type pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs index a43a69f06d479..e4d086d79d89a 100644 --- a/crates/bevy_remote/src/schemas/open_rpc.rs +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -1,4 +1,4 @@ -//! Module with trimmed down OpenRPC document structs. +//! Module with trimmed down `OpenRPC` document structs. //! It tries to follow this standard: use bevy_platform_support::collections::HashMap; use bevy_utils::default; @@ -6,11 +6,11 @@ use serde::{Deserialize, Serialize}; use crate::RemoteMethods; -/// Represents an OpenRPC document as defined by the OpenRPC specification. +/// Represents an `OpenRPC` document as defined by the `OpenRPC` specification. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenRpcDocument { - /// The version of the OpenRPC specification being used. + /// The version of the `OpenRPC` specification being used. pub openrpc: String, /// Informational metadata about the document. pub info: InfoObject, @@ -20,7 +20,7 @@ pub struct OpenRpcDocument { pub servers: Option>, } -/// Contains metadata information about the OpenRPC document. +/// Contains metadata information about the `OpenRPC` document. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct InfoObject { @@ -47,7 +47,7 @@ impl Default for InfoObject { } } -/// Describes a server hosting the API as specified in the OpenRPC document. +/// Describes a server hosting the API as specified in the `OpenRPC` document. #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct ServerObject { @@ -63,7 +63,7 @@ pub struct ServerObject { pub extensions: HashMap, } -/// Represents an RPC method in the OpenRPC document. +/// Represents an RPC method in the `OpenRPC` document. #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct MethodObject { From e081f65531b402b0f28022ec183e9d94fc778c3b Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Wed, 12 Mar 2025 14:24:09 +0100 Subject: [PATCH 4/8] Update crates/bevy_remote/src/schemas/open_rpc.rs Co-authored-by: Viktor Gustavsson --- crates/bevy_remote/src/schemas/open_rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs index e4d086d79d89a..82f863ce5a396 100644 --- a/crates/bevy_remote/src/schemas/open_rpc.rs +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -39,7 +39,7 @@ pub struct InfoObject { impl Default for InfoObject { fn default() -> Self { Self { - title: "Bevy OpenRPC".to_owned(), + title: "Bevy Remote Protocol".to_owned(), version: env!("CARGO_PKG_VERSION").to_owned(), description: None, extensions: Default::default(), From 73393778b1a167eb7d9d52f11f1b271bbd746a09 Mon Sep 17 00:00:00 2001 From: MevLyshkin Date: Wed, 12 Mar 2025 14:27:13 +0100 Subject: [PATCH 5/8] Version fix --- crates/bevy_remote/src/builtin_methods.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 2efd0b18ac076..b42cf41055e15 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -840,7 +840,7 @@ pub fn process_remote_list_methods_request( let doc = OpenRpcDocument { info: Default::default(), methods: remote_methods.into(), - openrpc: "1.0.0-rc1".to_owned(), + openrpc: "1.3.2".to_owned(), servers, }; From 3cb684ca714bbe74a7eea49d0dbd8c3c17699443 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 12 Mar 2025 17:34:55 +0100 Subject: [PATCH 6/8] Add paramater type --- crates/bevy_remote/src/schemas/open_rpc.rs | 24 +++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs index 82f863ce5a396..90a0aee70b742 100644 --- a/crates/bevy_remote/src/schemas/open_rpc.rs +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::RemoteMethods; +use super::json_schema::JsonSchemaBevyType; + /// Represents an `OpenRPC` document as defined by the `OpenRPC` specification. #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -75,9 +77,9 @@ pub struct MethodObject { /// An optional detailed description of the method. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, - // /// Parameters for the RPC method - // #[serde(default)] - // pub params: Vec, + /// Parameters for the RPC method + #[serde(default)] + pub params: Vec, // /// The expected result of the method // #[serde(skip_serializing_if = "Option::is_none")] // pub result: Option, @@ -86,6 +88,22 @@ pub struct MethodObject { pub extensions: HashMap, } +/// Represents an RPC method parameter in the `OpenRPC` document. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Parameter { + /// Parameter name + pub name: String, + /// Parameter description + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// JSON schema describing the parameter + pub schema: JsonSchemaBevyType, + /// Additional custom extension fields. + #[serde(flatten)] + pub extensions: HashMap, +} + impl From<&RemoteMethods> for Vec { fn from(value: &RemoteMethods) -> Self { value From 4e9cd5181860e4e3b2c420ae152c53342e73225b Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 12 Mar 2025 17:41:41 +0100 Subject: [PATCH 7/8] remove println --- crates/bevy_remote/src/schemas/json_schema.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index cf0d58dea96a7..f7a58006a54f9 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -374,7 +374,6 @@ mod tests { .expect("SHOULD BE REGISTERED") .clone(); let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -484,7 +483,6 @@ mod tests { .expect("SHOULD BE REGISTERED") .clone(); let (_, schema) = export_type(&foo_registration); - println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" From fd4b1e3086a7d96b4bff9649614c40c11df881f2 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 12 Mar 2025 22:26:09 +0100 Subject: [PATCH 8/8] Remove unused expect --- crates/bevy_remote/src/builtin_methods.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 3b252f17561fd..b42cf41055e15 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -1466,7 +1466,6 @@ fn get_resource_type_registration<'r>( } #[cfg(test)] -#[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { /// A generic function that tests serialization and deserialization of any type /// implementing Serialize and Deserialize traits.