First version
This commit is contained in:
parent
cfb679a752
commit
f96ed9f231
@ -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"]
|
||||
|
||||
|
@ -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.
|
||||
|
268
crates/bevy_remote/src/cmd.rs
Normal file
268
crates/bevy_remote/src/cmd.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user