First version

This commit is contained in:
Piotr Siuszko 2025-07-11 18:10:42 +02:00
parent cfb679a752
commit f96ed9f231
6 changed files with 708 additions and 298 deletions

View File

@ -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"]

View File

@ -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<Option<Value>>, 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<Self::ParameterType>,
world: &mut World,
) -> Result<Self::ResponseType, BrpError> {
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<Option<Value>>,
pub fn process_remote_list_methods_request_typed(
In(_): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let remote_methods = world.resource::<crate::RemoteMethods>();
) -> Result<OpenRpcDocument, BrpError> {
let remote_methods = world
.get_resource::<crate::RemoteMethods>()
.ok_or(BrpError::resource_not_present("bevy_remote::RemoteMethods"))?;
#[cfg(all(feature = "http", not(target_family = "wasm")))]
let servers = match (
world.get_resource::<crate::http::HostAddress>(),
world.get_resource::<crate::http::HostPort>(),
) {
(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<ServerObject> = remote_methods
.server_list()
.into_iter()
.map(|server| server.clone())
.collect();
let types = world.resource::<AppTypeRegistry>();
let types = types.read();
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>();
#[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.

View File

@ -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<Value>,
}
impl RpcCommand {
/// Create a new RPC command with the given path.
pub fn new(path: impl Into<Cow<'static, str>>) -> 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::<RemoteMethods>()
.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<T: Serialize + DeserializeOwned>(
params: Option<Value>,
) -> Result<Option<T>, BrpError> {
let command_input = match params {
Some(json_value) => {
match serde_json::from_value::<T>(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<Self::ParameterType>) -> Result<Self::ParameterType, BrpError> {
input.ok_or(BrpError::missing_input())
}
/// Builds the command with the given input.
fn to_command(input: Option<Self::ParameterType>) -> 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<T: RemoteCommand>() -> CommandTypeInfo {
CommandTypeInfo {
command_type: T::get_type_registration().type_id(),
arg_type: TypeId::of::<T::ParameterType>(),
response_type: TypeId::of::<T::ResponseType>(),
}
}
/// 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::<Self>)),
Some(get_command_type_info::<Self>()),
)
}
/// Implementation of the command method that processes input and returns a response.
fn method_impl(
input: Option<Self::ParameterType>,
world: &mut World,
) -> Result<Self::ResponseType, BrpError>;
}
fn command_system<T: RemoteCommandInstant>(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult {
let command_input = parse_input::<T::ParameterType>(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::<Self>)),
Some(get_command_type_info::<Self>()),
)
}
/// Implementation of the command method that processes input and returns an optional response.
fn method_impl(
input: Option<Self::ParameterType>,
world: &mut World,
) -> Result<Option<Self::ResponseType>, BrpError>;
}
fn watching_command_system<T: RemoteCommandWatching>(
In(params): In<Option<Value>>,
world: &mut World,
) -> BrpResult<Option<Value>> {
let command_input = parse_input::<T::ParameterType>(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<T: RemoteCommandInstant>(
mut methods: ResMut<RemoteMethods>,
mut commands: Commands,
) {
let system_id = commands.register_system(command_system::<T>);
methods.add_method::<T>(system_id);
}
fn add_remote_watching_command<T: RemoteCommandWatching>(
mut methods: ResMut<RemoteMethods>,
mut commands: Commands,
) {
let system_id = commands.register_system(watching_command_system::<T>);
methods.add_watching_method::<T>(system_id);
}
/// Extension trait for adding remote command methods to the Bevy App.
pub trait RemoteCommandAppExt {
/// Registers a remote method.
fn add_remote_method<T: RemoteCommandInstant>(&mut self) -> &mut Self;
/// Registers a remote method that can return multiple values.
fn add_remote_watching_method<T: RemoteCommandWatching>(&mut self) -> &mut Self;
/// Registers the types associated with a remote command for reflection.
fn register_method_types<T: RemoteCommand>(&mut self) -> &mut Self;
/// Registers a remote method that can return value once.
fn register_untyped_method<M>(
&mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, BrpResult, M>,
) -> &mut Self;
/// Registers a remote method that can return values multiple times.
fn register_untyped_watching_method<M>(
&mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, BrpResult<Option<Value>>, M>,
) -> &mut Self;
}
impl RemoteCommandAppExt for App {
fn add_remote_method<T: RemoteCommandInstant>(&mut self) -> &mut Self {
self.register_method_types::<T>()
.add_systems(PreStartup, add_remote_command::<T>)
}
fn add_remote_watching_method<T: RemoteCommandWatching>(&mut self) -> &mut Self {
self.register_method_types::<T>()
.add_systems(PreStartup, add_remote_watching_command::<T>)
}
fn register_method_types<T: RemoteCommand>(&mut self) -> &mut Self {
self.register_type::<T>()
.register_type::<T::ParameterType>()
.register_type::<T::ResponseType>()
}
fn register_untyped_method<M>(
&mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, 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::<RemoteMethods>()
.unwrap()
.insert(name, remote_handler);
self
}
fn register_untyped_watching_method<M>(
&mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, BrpResult<Option<Value>>, 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::<RemoteMethods>()
.unwrap()
.insert(name, remote_handler);
self
}
}

View File

@ -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<HeaderName, HeaderValue>,
}
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<HeaderName>,
value: impl TryInto<HeaderValue>,
) -> 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<HeaderName, HeaderValue>);
/// 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::<HttpServerConfig>();
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::<HttpServerConfig>),
);
}
}
fn update_server(
server_config: Option<ResMut<HttpServerConfig>>,
request_sender: Res<BrpSender>,
mut remote_methods: ResMut<RemoteMethods>,
) {
if server_config.is_none() {
remote_methods.remove_server(TypeId::of::<HttpServerConfig>());
}
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::<HttpServerConfig>(), (&*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<HeaderName>,
value: impl TryInto<HeaderValue>,
) -> 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<BrpSender>,
address: Res<HostAddress>,
remote_port: Res<HostPort>,
headers: Res<HostHeaders>,
) {
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<IpAddr> 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<IpAddressReflect> 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<Task<AnyhowResult<()>>>,
}
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<Async<TcpListener>> {
let ip: IpAddr = self.address.clone().into();
let listener = Async::<TcpListener>::bind((ip, self.port))?;
Ok(listener)
}
fn start_server(&mut self, request_sender: Res<BrpSender>) -> 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<TcpListener>,
request_sender: Sender<BrpMessage>,
headers: Headers,
) -> AnyhowResult<()> {
listen(
Async::<TcpListener>::bind((address, port))?,
&request_sender,
&headers,
)
.await
listen(listener, &request_sender, headers).await
}
async fn listen(
listener: Async<TcpListener>,
request_sender: &Sender<BrpMessage>,
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<TcpStream>,
request_sender: Sender<BrpMessage>,
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)

View File

@ -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<Vec<(String, RemoteMethodHandler)>>,
}
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<M>(
mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, 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<M>(
mut self,
name: impl Into<String>,
handler: impl IntoSystem<In<Option<Value>>, BrpResult<Option<Value>>, 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::<MainScheduleOrder>()
.insert_after(Last, RemoteLast);
app.insert_resource(remote_methods)
app.init_resource::<RemoteMethods>()
.add_remote_method::<RpcDiscoverCommand>()
.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::<schemas::SchemaTypesMetadata>()
.init_resource::<RemoteWatchingRequests>()
.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<dyn System<In = In<Option<Value>>, Out = BrpResult>>),
Instant(
Box<dyn System<In = In<Option<Value>>, Out = BrpResult>>,
Option<CommandTypeInfo>,
),
/// A handler that watches for changes and response when a change is detected.
Watching(Box<dyn System<In = In<Option<Value>>, Out = BrpResult<Option<Value>>>>),
Watching(
Box<dyn System<In = In<Option<Value>>, Out = BrpResult<Option<Value>>>>,
Option<CommandTypeInfo>,
),
}
/// 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<In<Option<Value>>, BrpResult<Op
#[derive(Debug, Clone, Copy)]
pub enum RemoteMethodSystemId {
/// A handler that only runs once and returns one response.
Instant(RemoteInstantMethodSystemId),
Instant(RemoteInstantMethodSystemId, Option<CommandTypeInfo>),
/// A handler that watches for changes and response when a change is detected.
Watching(RemoteWatchingMethodSystemId),
Watching(RemoteWatchingMethodSystemId, Option<CommandTypeInfo>),
}
impl RemoteMethodSystemId {
/// Returns the [`CommandTypeInfo`] of the remote method.
pub fn remote_type_info(&self) -> Option<CommandTypeInfo> {
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<String, RemoteMethodSystemId>);
pub struct RemoteMethods {
mappings: HashMap<Cow<'static, str>, RemoteMethodSystemId>,
servers: HashMap<TypeId, ServerObject>,
}
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<String>,
method_name: impl Into<Cow<'static, str>>,
handler: RemoteMethodSystemId,
) -> Option<RemoteMethodSystemId> {
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<T: RemoteCommandInstant>(&mut self, system_id: RemoteInstantMethodSystemId) {
self.insert(
T::RPC_PATH,
RemoteMethodSystemId::Instant(system_id, Some(get_command_type_info::<T>())),
);
}
/// Adds a new method, replacing any existing method with that name.
pub fn add_watching_method<T: RemoteCommandWatching>(
&mut self,
system_id: RemoteWatchingMethodSystemId,
) {
self.insert(
T::RPC_PATH,
RemoteMethodSystemId::Watching(system_id, Some(get_command_type_info::<T>())),
);
}
/// 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<String>`] with method names.
pub fn methods(&self) -> Vec<String> {
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::<RemoteWatchingRequests>()
.0

View File

@ -1,6 +1,7 @@
//! Module with trimmed down `OpenRPC` document structs.
//! It tries to follow this standard: <https://spec.open-rpc.org>
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<MethodObject>,
/// Optional list of server objects that provide the API endpoint details.
pub servers: Option<Vec<ServerObject>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
/// List of server objects that provide the API endpoint details.
pub servers: Vec<ServerObject>,
}
/// 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<String>,
/// A collection of custom extension fields.
#[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>,
}
@ -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<String>,
/// Additional custom extension fields.
#[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>,
}
/// 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<Parameter>,
// /// The expected result of the method
// #[serde(skip_serializing_if = "Option::is_none")]
// pub result: Option<Parameter>,
/// The expected result of the method
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Parameter>,
/// Additional custom extension fields.
#[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>,
}
/// 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<String>,
/// Parameter description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// JSON schema describing the parameter
#[reflect(ignore)]
pub schema: JsonSchemaBevyType,
/// Additional custom extension fields.
#[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>,
}
@ -110,7 +120,7 @@ impl From<&RemoteMethods> for Vec<MethodObject> {
.methods()
.iter()
.map(|e| MethodObject {
name: e.to_owned(),
name: e.to_string(),
..default()
})
.collect()