diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 899ac8b846..7167a96061 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -9,7 +9,8 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http", "bevy_asset"] +default = ["http", "bevy_asset", "documentation"] +documentation = ["bevy_reflect/documentation"] http = ["dep:async-io", "dep:smol-hyper"] bevy_asset = ["dep:bevy_asset"] diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index e847da08ed..c1c5250d6f 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -18,11 +18,18 @@ use bevy_log::warn_once; use bevy_platform::collections::HashMap; use bevy_reflect::{ serde::{ReflectSerializer, TypedReflectDeserializer}, - GetPath, PartialReflect, TypeRegistration, TypeRegistry, + GetPath, PartialReflect, Reflect, TypeRegistration, TypeRegistry, }; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; use serde_json::{Map, Value}; +use crate::{ + cmd::{RemoteCommand, RemoteCommandInstant}, + schemas::{ + json_schema::TypeRegistrySchemaReader, + open_rpc::{MethodObject, Parameter, ServerObject}, + }, +}; use crate::{ error_codes, schemas::{ @@ -32,9 +39,6 @@ use crate::{ BrpError, BrpResult, }; -#[cfg(all(feature = "http", not(target_family = "wasm")))] -use {crate::schemas::open_rpc::ServerObject, bevy_utils::default}; - /// The method path for a `bevy/get` request. pub const BRP_GET_METHOD: &str = "bevy/get"; @@ -953,42 +957,106 @@ pub fn process_remote_spawn_request(In(params): In>, world: &mut W serde_json::to_value(response).map_err(BrpError::internal) } +/// Returns an OpenRPC schema as a description of this service +#[derive(Reflect)] +pub struct RpcDiscoverCommand; + +impl RemoteCommand for RpcDiscoverCommand { + type ParameterType = (); + type ResponseType = OpenRpcDocument; + + const RPC_PATH: &str = RPC_DISCOVER_METHOD; +} + +impl RemoteCommandInstant for RpcDiscoverCommand { + fn method_impl( + _: Option, + world: &mut World, + ) -> Result { + process_remote_list_methods_request_typed(In(None), world) + } +} + /// Handles a `rpc.discover` request coming from a client. -pub fn process_remote_list_methods_request( - In(_params): In>, +pub fn process_remote_list_methods_request_typed( + In(_): In>, world: &mut World, -) -> BrpResult { - let remote_methods = world.resource::(); +) -> Result { + let remote_methods = world + .get_resource::() + .ok_or(BrpError::resource_not_present("bevy_remote::RemoteMethods"))?; - #[cfg(all(feature = "http", not(target_family = "wasm")))] - 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 servers: Vec = remote_methods + .server_list() + .into_iter() + .map(|server| server.clone()) + .collect(); + let types = world.resource::(); + let types = types.read(); + let extra_info = world.resource::(); - #[cfg(any(not(feature = "http"), target_family = "wasm"))] - let servers = None; + let methods = remote_methods + .mappings + .iter() + .map(|(name, info)| { + let Some(type_info) = info.remote_type_info() else { + return MethodObject { + name: name.to_string(), + ..Default::default() + }; + }; + #[cfg(feature = "documentation")] + let summary = types + .get(type_info.command_type) + .and_then(|t| t.type_info().docs().map(|s| s.to_string())); + #[cfg(not(feature = "documentation"))] + let summary = None; + let params = if type_info.arg_type.eq(&TypeId::of::<()>()) { + [].into() + } else { + let parameter = + match types.export_type_json_schema_for_id(extra_info, type_info.arg_type) { + Some(s) => Parameter { + name: "input".to_string(), + summary: Some(s.short_path.clone()), + // description: Some(s.description.clone()), + schema: s, + ..Default::default() + }, + None => Parameter::default(), + }; + [parameter].into() + }; + let result = if type_info.response_type.eq(&TypeId::of::<()>()) { + None + } else { + let result_schema = + types.export_type_json_schema_for_id(extra_info, type_info.response_type); + result_schema.map(|schema| Parameter { + name: "Result".to_string(), + summary: Some(schema.short_path.clone()), + schema, + ..Default::default() + }) + }; + MethodObject { + name: name.to_string(), + summary, + params, + result, + ..Default::default() + } + }) + .collect(); let doc = OpenRpcDocument { info: Default::default(), - methods: remote_methods.into(), + methods, openrpc: "1.3.2".to_owned(), servers, }; - serde_json::to_value(doc).map_err(BrpError::internal) + Ok(doc) } /// Handles a `bevy/insert` request (insert components) coming from a client. diff --git a/crates/bevy_remote/src/cmd.rs b/crates/bevy_remote/src/cmd.rs new file mode 100644 index 0000000000..b1b1a3b670 --- /dev/null +++ b/crates/bevy_remote/src/cmd.rs @@ -0,0 +1,268 @@ +//! Remote command handling module. +use std::{any::TypeId, borrow::Cow}; + +use bevy_app::{App, PreStartup}; +use bevy_ecs::{ + system::{Command, Commands, In, IntoSystem, ResMut}, + world::World, +}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::Value; + +use crate::{BrpError, BrpResult, CommandTypeInfo, RemoteMethodHandler, RemoteMethods}; + +/// Remote command handling module. +pub struct RpcCommand { + /// The path of the command. + pub path: Cow<'static, str>, + /// command input + pub input: Option, +} + +impl RpcCommand { + /// Create a new RPC command with the given path. + pub fn new(path: impl Into>) -> RpcCommand { + RpcCommand { + path: path.into(), + input: None, + } + } + + /// Set the input for the RPC command. + pub fn with_input(&mut self, input: Value) -> &mut Self { + self.input = Some(input); + self + } +} + +impl Command for RpcCommand { + fn apply(self, world: &mut World) { + let Some(remote_id) = world + .get_resource::() + .and_then(|e| e.get(&self.path)) + else { + return; + }; + match remote_id { + crate::RemoteMethodSystemId::Instant(system_id, ..) => { + let output = world.run_system_with(*system_id, self.input); + if let Ok(Ok(value)) = output { + bevy_log::info!("{}", serde_json::to_string_pretty(&value).expect("")); + } + } + crate::RemoteMethodSystemId::Watching(system_id, ..) => { + let _ = world.run_system_with(*system_id, self.input); + } + } + } +} + +/// Parses the input parameters for the command. +fn parse_input( + params: Option, +) -> Result, BrpError> { + let command_input = match params { + Some(json_value) => { + match serde_json::from_value::(json_value).map_err(BrpError::invalid_input) { + Ok(v) => Some(v), + Err(e) => return Err(e), + } + } + None => None, + }; + Ok(command_input) +} + +/// Helper trait for creating RPC commands. +pub trait RemoteCommand: bevy_reflect::GetTypeRegistration + Sized { + /// Type of the input parameter for the command. + type ParameterType: Serialize + DeserializeOwned + bevy_reflect::GetTypeRegistration; + /// Type of the response for the command. + type ResponseType: Serialize + DeserializeOwned + bevy_reflect::GetTypeRegistration; + /// Path of the command. + const RPC_PATH: &str; + + /// Returns the input parameter for the command. + fn input_or_err(input: Option) -> Result { + input.ok_or(BrpError::missing_input()) + } + + /// Builds the command with the given input. + fn to_command(input: Option) -> RpcCommand { + RpcCommand { + path: Self::RPC_PATH.into(), + input: serde_json::to_value(input).ok(), + } + } + + /// Builds the command with no input. + fn no_input() -> RpcCommand { + RpcCommand { + path: Self::RPC_PATH.into(), + input: None, + } + } +} + +/// Returns the type information for the command. +pub(crate) fn get_command_type_info() -> CommandTypeInfo { + CommandTypeInfo { + command_type: T::get_type_registration().type_id(), + arg_type: TypeId::of::(), + response_type: TypeId::of::(), + } +} +/// Trait for remote commands that execute instantly and return a response. +pub trait RemoteCommandInstant: RemoteCommand { + /// Returns the method handler for this instant remote command. + fn get_method_handler() -> RemoteMethodHandler { + RemoteMethodHandler::Instant( + Box::new(IntoSystem::into_system(command_system::)), + Some(get_command_type_info::()), + ) + } + /// Implementation of the command method that processes input and returns a response. + fn method_impl( + input: Option, + world: &mut World, + ) -> Result; +} + +fn command_system( + In(params): In>, + world: &mut World, +) -> BrpResult { + let command_input = parse_input::(params)?; + + match T::method_impl(command_input, world) { + Ok(v) => match serde_json::to_value(v) { + Ok(value) => Ok(value), + Err(e) => Err(BrpError::internal(e)), + }, + Err(e) => Err(e), + } +} +/// Trait for remote commands that execute continuously and may return optional responses. +pub trait RemoteCommandWatching: RemoteCommand { + /// Returns the method handler for this watching remote command. + fn get_method_handler() -> RemoteMethodHandler { + RemoteMethodHandler::Watching( + Box::new(IntoSystem::into_system(watching_command_system::)), + Some(get_command_type_info::()), + ) + } + /// Implementation of the command method that processes input and returns an optional response. + fn method_impl( + input: Option, + world: &mut World, + ) -> Result, BrpError>; +} + +fn watching_command_system( + In(params): In>, + world: &mut World, +) -> BrpResult> { + let command_input = parse_input::(params)?; + let command_output = T::method_impl(command_input, world)?; + match command_output { + Some(v) => { + let value = serde_json::to_value(v).map_err(BrpError::internal)?; + Ok(Some(value)) + } + None => Ok(None), + } +} +fn add_remote_command( + mut methods: ResMut, + mut commands: Commands, +) { + let system_id = commands.register_system(command_system::); + methods.add_method::(system_id); +} + +fn add_remote_watching_command( + mut methods: ResMut, + mut commands: Commands, +) { + let system_id = commands.register_system(watching_command_system::); + methods.add_watching_method::(system_id); +} +/// Extension trait for adding remote command methods to the Bevy App. +pub trait RemoteCommandAppExt { + /// Registers a remote method. + fn add_remote_method(&mut self) -> &mut Self; + /// Registers a remote method that can return multiple values. + fn add_remote_watching_method(&mut self) -> &mut Self; + /// Registers the types associated with a remote command for reflection. + fn register_method_types(&mut self) -> &mut Self; + + /// Registers a remote method that can return value once. + fn register_untyped_method( + &mut self, + name: impl Into, + handler: impl IntoSystem>, BrpResult, M>, + ) -> &mut Self; + /// Registers a remote method that can return values multiple times. + fn register_untyped_watching_method( + &mut self, + name: impl Into, + handler: impl IntoSystem>, BrpResult>, M>, + ) -> &mut Self; +} + +impl RemoteCommandAppExt for App { + fn add_remote_method(&mut self) -> &mut Self { + self.register_method_types::() + .add_systems(PreStartup, add_remote_command::) + } + fn add_remote_watching_method(&mut self) -> &mut Self { + self.register_method_types::() + .add_systems(PreStartup, add_remote_watching_command::) + } + + fn register_method_types(&mut self) -> &mut Self { + self.register_type::() + .register_type::() + .register_type::() + } + + fn register_untyped_method( + &mut self, + name: impl Into, + handler: impl IntoSystem>, BrpResult, M>, + ) -> &mut Self { + let remote_handler = crate::RemoteMethodSystemId::Instant( + self.main_mut() + .world_mut() + .register_boxed_system(Box::new(IntoSystem::into_system(handler))), + None, + ); + let name = name.into(); + self.main_mut() + .world_mut() + .get_resource_mut::() + .unwrap() + .insert(name, remote_handler); + self + } + + fn register_untyped_watching_method( + &mut self, + name: impl Into, + handler: impl IntoSystem>, BrpResult>, M>, + ) -> &mut Self { + let remote_handler = crate::RemoteMethodSystemId::Watching( + self.main_mut() + .world_mut() + .register_boxed_system(Box::new(IntoSystem::into_system(handler))), + None, + ); + let name = name.into(); + self.main_mut() + .world_mut() + .get_resource_mut::() + .unwrap() + .insert(name, remote_handler); + self + } +} diff --git a/crates/bevy_remote/src/http.rs b/crates/bevy_remote/src/http.rs index 4e36e4a0bf..edba494416 100644 --- a/crates/bevy_remote/src/http.rs +++ b/crates/bevy_remote/src/http.rs @@ -7,16 +7,26 @@ //! example for a trivial example of use. #![cfg(not(target_family = "wasm"))] - +use crate::schemas::open_rpc::ServerObject; use crate::{ error_codes, BrpBatch, BrpError, BrpMessage, BrpRequest, BrpResponse, BrpResult, BrpSender, + RemoteMethods, }; use anyhow::Result as AnyhowResult; use async_channel::{Receiver, Sender}; use async_io::Async; -use bevy_app::{App, Plugin, Startup}; +use bevy_app::{App, Plugin, Update}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::change_detection::DetectChanges; +use bevy_ecs::prelude::ReflectResource; use bevy_ecs::resource::Resource; -use bevy_ecs::system::Res; +use bevy_ecs::schedule::common_conditions::resource_changed_or_removed; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_ecs::system::{Res, ResMut}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use bevy_tasks::Task; use bevy_tasks::{futures_lite::StreamExt, IoTaskPool}; use core::{ convert::Infallible, @@ -31,12 +41,12 @@ use hyper::{ server::conn::http1, service, Request, Response, }; +use serde::{Deserialize, Serialize}; use serde_json::Value; use smol_hyper::rt::{FuturesIo, SmolTimer}; -use std::{ - collections::HashMap, - net::{TcpListener, TcpStream}, -}; +use std::any::TypeId; +use std::net::Ipv6Addr; +use std::net::{TcpListener, TcpStream}; /// The default port that Bevy will listen on. /// @@ -51,41 +61,8 @@ pub const DEFAULT_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); /// This struct is used to store a set of HTTP headers as key-value pairs, where the keys are /// of type [`HeaderName`] and the values are of type [`HeaderValue`]. /// -#[derive(Debug, Resource, Clone)] -pub struct Headers { - headers: HashMap, -} - -impl Headers { - /// Create a new instance of `Headers`. - pub fn new() -> Self { - Self { - headers: HashMap::default(), - } - } - - /// Insert a key value pair to the `Headers` instance. - pub fn insert( - mut self, - name: impl TryInto, - value: impl TryInto, - ) -> Self { - let Ok(header_name) = name.try_into() else { - panic!("Invalid header name") - }; - let Ok(header_value) = value.try_into() else { - panic!("Invalid header value") - }; - self.headers.insert(header_name, header_value); - self - } -} - -impl Default for Headers { - fn default() -> Self { - Self::new() - } -} +#[derive(Debug, Clone, Deref, DerefMut, Default)] +pub struct Headers(HashMap); /// Add this plugin to your [`App`] to allow remote connections over HTTP to inspect and modify entities. /// It requires the [`RemotePlugin`](super::RemotePlugin). @@ -110,17 +87,50 @@ impl Default for RemoteHttpPlugin { Self { address: DEFAULT_ADDR, port: DEFAULT_PORT, - headers: Headers::new(), + headers: Headers::default(), } } } impl Plugin for RemoteHttpPlugin { fn build(&self, app: &mut App) { - app.insert_resource(HostAddress(self.address)) - .insert_resource(HostPort(self.port)) - .insert_resource(HostHeaders(self.headers.clone())) - .add_systems(Startup, start_http_server); + app.register_type::(); + app.insert_resource(HttpServerConfig { + address: self.address.into(), + port: self.port, + headers: self.headers.clone(), + task: None, + }) + .add_systems( + Update, + update_server.run_if(resource_changed_or_removed::), + ); + } +} + +fn update_server( + server_config: Option>, + request_sender: Res, + mut remote_methods: ResMut, +) { + if server_config.is_none() { + remote_methods.remove_server(TypeId::of::()); + } + bevy_log::info!("exist: {}", server_config.is_some()); + if let Some(mut config) = server_config { + let should_start_server = (config.is_added() && config.task.is_none()) + || (config.is_changed() && config.task.is_some()); + bevy_log::info!( + "added: {}, changed: {}, should_start_server: {}, config: {:?}", + config.is_added(), + config.is_changed(), + should_start_server, + &config + ); + + if should_start_server && config.start_server(request_sender).is_ok() { + remote_methods.register_server(TypeId::of::(), (&*config).into()); + }; } } @@ -152,7 +162,7 @@ impl RemoteHttpPlugin { /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) - /// .add_plugins(RemotePlugin::default()) + /// .add_plugins(RemotePlugin) /// .add_plugins(RemoteHttpPlugin::default() /// .with_headers(cors_headers)) /// .run(); @@ -170,66 +180,102 @@ impl RemoteHttpPlugin { name: impl TryInto, value: impl TryInto, ) -> Self { - self.headers = self.headers.insert(name, value); + match (name.try_into(), value.try_into()) { + (Ok(name), Ok(value)) => _ = self.headers.insert(name, value), + _ => {} + } self } } - -/// A resource containing the IP address that Bevy will host on. +/// A reflectable representation of an IP address. /// -/// Currently, changing this while the application is running has no effect; this merely -/// reflects the IP address that is set during the setup of the [`RemoteHttpPlugin`]. -#[derive(Debug, Resource)] -pub struct HostAddress(pub IpAddr); - -/// A resource containing the port number that Bevy will listen on. -/// -/// Currently, changing this while the application is running has no effect; this merely -/// reflects the host that is set during the setup of the [`RemoteHttpPlugin`]. -#[derive(Debug, Resource)] -pub struct HostPort(pub u16); - -/// A resource containing the headers that Bevy will include in its HTTP responses. -/// -#[derive(Debug, Resource)] -struct HostHeaders(pub Headers); - -/// A system that starts up the Bevy Remote Protocol HTTP server. -fn start_http_server( - request_sender: Res, - address: Res, - remote_port: Res, - headers: Res, -) { - IoTaskPool::get() - .spawn(server_main( - address.0, - remote_port.0, - request_sender.clone(), - headers.0.clone(), - )) - .detach(); +/// This enum provides a serializable and reflectable alternative to [`std::net::IpAddr`] +/// for use in Bevy's reflection system. It can represent both IPv4 and IPv6 addresses +/// as byte arrays. +#[derive(Debug, Resource, Reflect, Clone, Serialize, Deserialize)] +#[reflect(Serialize, Deserialize)] +pub enum IpAddressReflect { + /// An IPv4 address represented as a 4-byte array. + V4([u8; 4]), + /// An IPv6 address represented as a 16-byte array. + V6([u8; 16]), } +impl From for IpAddressReflect { + fn from(value: IpAddr) -> Self { + match value { + IpAddr::V4(addr) => IpAddressReflect::V4(addr.octets()), + IpAddr::V6(addr) => IpAddressReflect::V6(addr.octets()), + } + } +} + +impl From for IpAddr { + fn from(value: IpAddressReflect) -> Self { + match value { + IpAddressReflect::V4(addr) => IpAddr::V4(Ipv4Addr::from(addr)), + IpAddressReflect::V6(addr) => IpAddr::V6(Ipv6Addr::from(addr)), + } + } +} + +#[derive(Debug, Resource, Reflect, Serialize, Deserialize)] +#[reflect(Resource, Serialize, Deserialize)] +/// A resource containing the data for the HTTP server. +pub struct HttpServerConfig { + /// The address to bind the server to. + pub address: IpAddressReflect, + /// The port to bind the server to. + pub port: u16, + #[reflect(ignore)] + #[serde(skip)] + /// The headers to send with each response. + pub headers: Headers, + #[reflect(ignore)] + #[serde(skip)] + /// The task that is running the server. + pub task: Option>>, +} + +impl From<&HttpServerConfig> for ServerObject { + fn from(value: &HttpServerConfig) -> Self { + let ip: IpAddr = value.address.clone().into(); + ServerObject { + name: HttpServerConfig::short_type_path().into(), + url: format!("{}:{}", ip, value.port), + ..Default::default() + } + } +} + +impl HttpServerConfig { + fn build_listener(&self) -> AnyhowResult> { + let ip: IpAddr = self.address.clone().into(); + let listener = Async::::bind((ip, self.port))?; + Ok(listener) + } + + fn start_server(&mut self, request_sender: Res) -> AnyhowResult<()> { + let listener = self.build_listener()?; + let headers = self.headers.clone(); + self.task = + Some(IoTaskPool::get().spawn(server_main(listener, request_sender.clone(), headers))); + Ok(()) + } +} /// The Bevy Remote Protocol server main loop. async fn server_main( - address: IpAddr, - port: u16, + listener: Async, request_sender: Sender, headers: Headers, ) -> AnyhowResult<()> { - listen( - Async::::bind((address, port))?, - &request_sender, - &headers, - ) - .await + listen(listener, &request_sender, headers).await } async fn listen( listener: Async, request_sender: &Sender, - headers: &Headers, + headers: Headers, ) -> AnyhowResult<()> { loop { let (client, _) = listener.accept().await?; @@ -238,7 +284,7 @@ async fn listen( let headers = headers.clone(); IoTaskPool::get() .spawn(async move { - let _ = handle_client(client, request_sender, headers).await; + let _ = handle_client(client, request_sender, &headers).await; }) .detach(); } @@ -247,15 +293,13 @@ async fn listen( async fn handle_client( client: Async, request_sender: Sender, - headers: Headers, + headers: &Headers, ) -> AnyhowResult<()> { http1::Builder::new() .timer(SmolTimer::new()) .serve_connection( FuturesIo::new(client), - service::service_fn(|request| { - process_request_batch(request, &request_sender, &headers) - }), + service::service_fn(|request| process_request_batch(request, &request_sender, headers)), ) .await?; @@ -338,7 +382,7 @@ async fn process_request_batch( response } }; - for (key, value) in &headers.headers { + for (key, value) in headers.iter() { response.headers_mut().insert(key, value.clone()); } Ok(response) diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 25c1835faf..9ec50ef4ee 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -467,12 +467,9 @@ //! fn main() { //! App::new() //! .add_plugins(DefaultPlugins) -//! .add_plugins( -//! // `default` adds all of the built-in methods, while `with_method` extends them -//! RemotePlugin::default() -//! .with_method("super_user/cool_method", path::to::my::cool::handler) -//! // ... more methods can be added by chaining `with_method` -//! ) +//! .add_plugins(RemotePlugin) +//! // more methods can be added by chaining `register_untyped_method` +//! .register_untyped_method("super_user/cool_method", path::to::my::cool::handler) //! .add_systems( //! // ... standard application setup //! ) @@ -508,16 +505,25 @@ use bevy_ecs::{ entity::Entity, resource::Resource, schedule::{IntoScheduleConfigs, ScheduleLabel, SystemSet}, - system::{Commands, In, IntoSystem, ResMut, System, SystemId}, + system::{Commands, In, ResMut, System, SystemId}, world::World, }; use bevy_platform::collections::HashMap; use bevy_utils::prelude::default; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::sync::RwLock; +use std::{any::TypeId, borrow::Cow}; + +use crate::{ + builtin_methods::RpcDiscoverCommand, + cmd::{ + get_command_type_info, RemoteCommandAppExt, RemoteCommandInstant, RemoteCommandWatching, + }, + schemas::open_rpc::ServerObject, +}; pub mod builtin_methods; +pub mod cmd; #[cfg(feature = "http")] pub mod http; pub mod schemas; @@ -530,152 +536,85 @@ const CHANNEL_SIZE: usize = 16; /// the available protocols and its default methods. /// /// [crate-level documentation]: crate -pub struct RemotePlugin { - /// The verbs that the server will recognize and respond to. - methods: RwLock>, -} - -impl RemotePlugin { - /// Create a [`RemotePlugin`] with the default address and port but without - /// any associated methods. - fn empty() -> Self { - Self { - methods: RwLock::new(vec![]), - } - } - - /// Add a remote method to the plugin using the given `name` and `handler`. - #[must_use] - pub fn with_method( - mut self, - name: impl Into, - handler: impl IntoSystem>, BrpResult, M>, - ) -> Self { - self.methods.get_mut().unwrap().push(( - name.into(), - RemoteMethodHandler::Instant(Box::new(IntoSystem::into_system(handler))), - )); - self - } - - /// Add a remote method with a watching handler to the plugin using the given `name`. - #[must_use] - pub fn with_watching_method( - mut self, - name: impl Into, - handler: impl IntoSystem>, BrpResult>, M>, - ) -> Self { - self.methods.get_mut().unwrap().push(( - name.into(), - RemoteMethodHandler::Watching(Box::new(IntoSystem::into_system(handler))), - )); - self - } -} - -impl Default for RemotePlugin { - fn default() -> Self { - Self::empty() - .with_method( - builtin_methods::BRP_GET_METHOD, - builtin_methods::process_remote_get_request, - ) - .with_method( - builtin_methods::BRP_QUERY_METHOD, - builtin_methods::process_remote_query_request, - ) - .with_method( - builtin_methods::BRP_SPAWN_METHOD, - builtin_methods::process_remote_spawn_request, - ) - .with_method( - builtin_methods::BRP_INSERT_METHOD, - builtin_methods::process_remote_insert_request, - ) - .with_method( - builtin_methods::BRP_REMOVE_METHOD, - builtin_methods::process_remote_remove_request, - ) - .with_method( - builtin_methods::BRP_DESTROY_METHOD, - builtin_methods::process_remote_destroy_request, - ) - .with_method( - builtin_methods::BRP_REPARENT_METHOD, - builtin_methods::process_remote_reparent_request, - ) - .with_method( - builtin_methods::BRP_LIST_METHOD, - builtin_methods::process_remote_list_request, - ) - .with_method( - 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, - ) - .with_watching_method( - builtin_methods::BRP_LIST_AND_WATCH_METHOD, - builtin_methods::process_remote_list_watching_request, - ) - .with_method( - builtin_methods::BRP_GET_RESOURCE_METHOD, - builtin_methods::process_remote_get_resource_request, - ) - .with_method( - builtin_methods::BRP_INSERT_RESOURCE_METHOD, - builtin_methods::process_remote_insert_resource_request, - ) - .with_method( - builtin_methods::BRP_REMOVE_RESOURCE_METHOD, - builtin_methods::process_remote_remove_resource_request, - ) - .with_method( - builtin_methods::BRP_MUTATE_RESOURCE_METHOD, - builtin_methods::process_remote_mutate_resource_request, - ) - .with_method( - builtin_methods::BRP_LIST_RESOURCES_METHOD, - builtin_methods::process_remote_list_resources_request, - ) - .with_method( - builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, - builtin_methods::export_registry_types, - ) - } -} +pub struct RemotePlugin; impl Plugin for RemotePlugin { fn build(&self, app: &mut App) { - let mut remote_methods = RemoteMethods::new(); - - let plugin_methods = &mut *self.methods.write().unwrap(); - for (name, handler) in plugin_methods.drain(..) { - remote_methods.insert( - name, - match handler { - RemoteMethodHandler::Instant(system) => RemoteMethodSystemId::Instant( - app.main_mut().world_mut().register_boxed_system(system), - ), - RemoteMethodHandler::Watching(system) => RemoteMethodSystemId::Watching( - app.main_mut().world_mut().register_boxed_system(system), - ), - }, - ); - } - app.init_schedule(RemoteLast) .world_mut() .resource_mut::() .insert_after(Last, RemoteLast); - app.insert_resource(remote_methods) + app.init_resource::() + .add_remote_method::() + .register_untyped_method( + builtin_methods::BRP_GET_METHOD, + builtin_methods::process_remote_get_request, + ) + .register_untyped_method( + builtin_methods::BRP_QUERY_METHOD, + builtin_methods::process_remote_query_request, + ) + .register_untyped_method( + builtin_methods::BRP_SPAWN_METHOD, + builtin_methods::process_remote_spawn_request, + ) + .register_untyped_method( + builtin_methods::BRP_INSERT_METHOD, + builtin_methods::process_remote_insert_request, + ) + .register_untyped_method( + builtin_methods::BRP_REMOVE_METHOD, + builtin_methods::process_remote_remove_request, + ) + .register_untyped_method( + builtin_methods::BRP_DESTROY_METHOD, + builtin_methods::process_remote_destroy_request, + ) + .register_untyped_method( + builtin_methods::BRP_REPARENT_METHOD, + builtin_methods::process_remote_reparent_request, + ) + .register_untyped_method( + builtin_methods::BRP_LIST_METHOD, + builtin_methods::process_remote_list_request, + ) + .register_untyped_method( + builtin_methods::BRP_MUTATE_COMPONENT_METHOD, + builtin_methods::process_remote_mutate_component_request, + ) + .register_untyped_watching_method( + builtin_methods::BRP_GET_AND_WATCH_METHOD, + builtin_methods::process_remote_get_watching_request, + ) + .register_untyped_watching_method( + builtin_methods::BRP_LIST_AND_WATCH_METHOD, + builtin_methods::process_remote_list_watching_request, + ) + .register_untyped_method( + builtin_methods::BRP_GET_RESOURCE_METHOD, + builtin_methods::process_remote_get_resource_request, + ) + .register_untyped_method( + builtin_methods::BRP_INSERT_RESOURCE_METHOD, + builtin_methods::process_remote_insert_resource_request, + ) + .register_untyped_method( + builtin_methods::BRP_REMOVE_RESOURCE_METHOD, + builtin_methods::process_remote_remove_resource_request, + ) + .register_untyped_method( + builtin_methods::BRP_MUTATE_RESOURCE_METHOD, + builtin_methods::process_remote_mutate_resource_request, + ) + .register_untyped_method( + builtin_methods::BRP_LIST_RESOURCES_METHOD, + builtin_methods::process_remote_list_resources_request, + ) + .register_untyped_method( + builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, + builtin_methods::export_registry_types, + ) .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) @@ -718,9 +657,28 @@ pub type RemoteSet = RemoteSystems; #[derive(Debug)] pub enum RemoteMethodHandler { /// A handler that only runs once and returns one response. - Instant(Box>, Out = BrpResult>>), + Instant( + Box>, Out = BrpResult>>, + Option, + ), /// A handler that watches for changes and response when a change is detected. - Watching(Box>, Out = BrpResult>>>), + Watching( + Box>, Out = BrpResult>>>, + Option, + ), +} + +/// Type information for remote commands. +/// +/// This struct contains the [`TypeId`]s for the command type, its arguments, and its response. +#[derive(Clone, Debug, Copy)] +pub struct CommandTypeInfo { + /// The [`TypeId`] of the command type. + pub command_type: TypeId, + /// The [`TypeId`] of the argument type. + pub arg_type: TypeId, + /// The [`TypeId`] of the response type. + pub response_type: TypeId, } /// The [`SystemId`] of a function that implements a remote instant method (`bevy/get`, `bevy/query`, etc.) @@ -746,16 +704,29 @@ pub type RemoteWatchingMethodSystemId = SystemId>, BrpResult), /// A handler that watches for changes and response when a change is detected. - Watching(RemoteWatchingMethodSystemId), + Watching(RemoteWatchingMethodSystemId, Option), +} + +impl RemoteMethodSystemId { + /// Returns the [`CommandTypeInfo`] of the remote method. + pub fn remote_type_info(&self) -> Option { + match self { + RemoteMethodSystemId::Instant(_, type_info) + | RemoteMethodSystemId::Watching(_, type_info) => *type_info, + } + } } /// Holds all implementations of methods known to the server. /// /// Custom methods can be added to this list using [`RemoteMethods::insert`]. #[derive(Debug, Resource, Default)] -pub struct RemoteMethods(HashMap); +pub struct RemoteMethods { + mappings: HashMap, RemoteMethodSystemId>, + servers: HashMap, +} impl RemoteMethods { /// Creates a new [`RemoteMethods`] resource with no methods registered in it. @@ -768,20 +739,54 @@ impl RemoteMethods { /// If there was an existing method with that name, returns its handler. pub fn insert( &mut self, - method_name: impl Into, + method_name: impl Into>, handler: RemoteMethodSystemId, ) -> Option { - self.0.insert(method_name.into(), handler) + self.mappings.insert(method_name.into(), handler) + } + + /// Adds a new method, replacing any existing method with that name. + pub fn add_method(&mut self, system_id: RemoteInstantMethodSystemId) { + self.insert( + T::RPC_PATH, + RemoteMethodSystemId::Instant(system_id, Some(get_command_type_info::())), + ); + } + + /// Adds a new method, replacing any existing method with that name. + pub fn add_watching_method( + &mut self, + system_id: RemoteWatchingMethodSystemId, + ) { + self.insert( + T::RPC_PATH, + RemoteMethodSystemId::Watching(system_id, Some(get_command_type_info::())), + ); } /// Get a [`RemoteMethodSystemId`] with its method name. pub fn get(&self, method: &str) -> Option<&RemoteMethodSystemId> { - self.0.get(method) + self.mappings.get(method) } - /// Get a [`Vec`] with method names. - pub fn methods(&self) -> Vec { - self.0.keys().cloned().collect() + /// Get a [`Vec<&Cow<'_, str>>`] with method names. + pub fn methods(&self) -> Vec<&Cow<'_, str>> { + self.mappings.keys().collect() + } + + /// Get a [`Vec<&ServerObject>`] with server objects. + pub fn server_list(&self) -> Vec<&ServerObject> { + self.servers.values().collect() + } + + /// Registers a new server object mapping. + pub fn register_server(&mut self, server_id: TypeId, server: ServerObject) { + self.servers.insert(server_id, server); + } + + /// Removes a server object mapping. + pub fn remove_server(&mut self, server_id: TypeId) { + self.servers.remove(&server_id); } } @@ -916,6 +921,20 @@ impl BrpError { } } + /// BRP command input was invalid and it could not be parsed. + pub fn invalid_input(message: impl ToString) -> Self { + Self { + code: error_codes::INVALID_PARAMS, + message: message.to_string(), + data: None, + } + } + + /// BRP command input was required and was not provided. + pub fn missing_input() -> Self { + Self::invalid_input("Params not provided") + } + /// Component wasn't found in an entity. #[must_use] pub fn component_not_present(component: &str, entity: Entity) -> Self { @@ -1091,7 +1110,7 @@ fn process_remote_requests(world: &mut World) { }; match handler { - RemoteMethodSystemId::Instant(id) => { + RemoteMethodSystemId::Instant(id, ..) => { let result = match world.run_system_with(id, message.params) { Ok(result) => result, Err(error) => { @@ -1106,7 +1125,7 @@ fn process_remote_requests(world: &mut World) { let _ = message.sender.force_send(result); } - RemoteMethodSystemId::Watching(id) => { + RemoteMethodSystemId::Watching(id, ..) => { world .resource_mut::() .0 diff --git a/crates/bevy_remote/src/schemas/open_rpc.rs b/crates/bevy_remote/src/schemas/open_rpc.rs index 0ffda36bc3..691e196903 100644 --- a/crates/bevy_remote/src/schemas/open_rpc.rs +++ b/crates/bevy_remote/src/schemas/open_rpc.rs @@ -1,6 +1,7 @@ //! Module with trimmed down `OpenRPC` document structs. //! It tries to follow this standard: use bevy_platform::collections::HashMap; +use bevy_reflect::Reflect; use bevy_utils::default; use serde::{Deserialize, Serialize}; @@ -9,7 +10,7 @@ use crate::RemoteMethods; use super::json_schema::JsonSchemaBevyType; /// Represents an `OpenRPC` document as defined by the `OpenRPC` specification. -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Reflect)] #[serde(rename_all = "camelCase")] pub struct OpenRpcDocument { /// The version of the `OpenRPC` specification being used. @@ -18,12 +19,13 @@ pub struct OpenRpcDocument { 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>, + #[serde(skip_serializing_if = "Vec::is_empty")] + /// List of server objects that provide the API endpoint details. + pub servers: Vec, } /// Contains metadata information about the `OpenRPC` document. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Reflect)] #[serde(rename_all = "camelCase")] pub struct InfoObject { /// The title of the API or document. @@ -35,6 +37,7 @@ pub struct InfoObject { pub description: Option, /// A collection of custom extension fields. #[serde(flatten)] + #[reflect(ignore)] pub extensions: HashMap, } @@ -50,7 +53,7 @@ impl Default for InfoObject { } /// Describes a server hosting the API as specified in the `OpenRPC` document. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, Reflect)] #[serde(rename_all = "camelCase")] pub struct ServerObject { /// The name of the server. @@ -62,11 +65,12 @@ pub struct ServerObject { pub description: Option, /// Additional custom extension fields. #[serde(flatten)] + #[reflect(ignore)] pub extensions: HashMap, } /// Represents an RPC method in the `OpenRPC` document. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Reflect)] #[serde(rename_all = "camelCase")] pub struct MethodObject { /// The method name (e.g., "/bevy/get") @@ -80,27 +84,33 @@ pub struct MethodObject { /// 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, + /// The expected result of the method + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, /// Additional custom extension fields. #[serde(flatten)] + #[reflect(ignore)] pub extensions: HashMap, } /// Represents an RPC method parameter in the `OpenRPC` document. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone, Reflect, Default)] #[serde(rename_all = "camelCase")] pub struct Parameter { /// Parameter name pub name: String, + /// An optional short summary of the method. + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, /// Parameter description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// JSON schema describing the parameter + #[reflect(ignore)] pub schema: JsonSchemaBevyType, /// Additional custom extension fields. #[serde(flatten)] + #[reflect(ignore)] pub extensions: HashMap, } @@ -110,7 +120,7 @@ impl From<&RemoteMethods> for Vec { .methods() .iter() .map(|e| MethodObject { - name: e.to_owned(), + name: e.to_string(), ..default() }) .collect()