From e3be7a339faeadee03fc544bc5981277fc52f62e Mon Sep 17 00:00:00 2001 From: Marc Guiselin Date: Fri, 15 May 2026 11:47:18 +0200 Subject: [PATCH 1/5] Deserializable BrpResponse --- crates/bevy_remote/src/http.rs | 2 +- crates/bevy_remote/src/lib.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_remote/src/http.rs b/crates/bevy_remote/src/http.rs index 092cc2f1b7872..439d2e2f76e32 100644 --- a/crates/bevy_remote/src/http.rs +++ b/crates/bevy_remote/src/http.rs @@ -386,7 +386,7 @@ async fn process_request_batch( async fn process_single_request( request: Value, request_sender: &Sender, -) -> AnyhowResult> { +) -> AnyhowResult, BrpStream>> { // Reach in and get the request ID early so that we can report it even when parsing fails. let id = request.as_object().and_then(|map| map.get("id")).cloned(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index ec1f9b3b2c57f..be4fa6dc7fae8 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -534,6 +534,7 @@ extern crate alloc; +use alloc::borrow::Cow; use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -1156,9 +1157,9 @@ impl<'de> Deserialize<'de> for BrpRequest { /// A response according to BRP. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BrpResponse { +pub struct BrpResponse<'a> { /// This field is mandatory and must be set to `"2.0"`. - pub jsonrpc: &'static str, + pub jsonrpc: Cow<'a, str>, /// The id of the original request. pub id: Option, @@ -1168,12 +1169,12 @@ pub struct BrpResponse { pub payload: BrpPayload, } -impl BrpResponse { +impl BrpResponse<'static> { /// Generates a [`BrpResponse`] from an id and a `Result`. #[must_use] pub fn new(id: Option, result: BrpResult) -> Self { Self { - jsonrpc: "2.0", + jsonrpc: Cow::Borrowed("2.0"), id, payload: BrpPayload::from(result), } From 94c2a42514232b551340ef844ef910ac7ac2dd7c Mon Sep 17 00:00:00 2001 From: Marc Guiselin Date: Fri, 12 Jun 2026 16:14:02 +0200 Subject: [PATCH 2/5] Custom impl for Serialize and Deserialize --- crates/bevy_remote/src/http.rs | 2 +- crates/bevy_remote/src/lib.rs | 123 ++++++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/crates/bevy_remote/src/http.rs b/crates/bevy_remote/src/http.rs index 439d2e2f76e32..092cc2f1b7872 100644 --- a/crates/bevy_remote/src/http.rs +++ b/crates/bevy_remote/src/http.rs @@ -386,7 +386,7 @@ async fn process_request_batch( async fn process_single_request( request: Value, request_sender: &Sender, -) -> AnyhowResult, BrpStream>> { +) -> AnyhowResult> { // Reach in and get the request ID early so that we can report it even when parsing fails. let id = request.as_object().and_then(|map| map.get("id")).cloned(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index be4fa6dc7fae8..7e1f945224eb0 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -534,7 +534,6 @@ extern crate alloc; -use alloc::borrow::Cow; use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -1052,7 +1051,7 @@ pub struct BrpRequest { } // BRP uses json-rpc 2.0, so we need to include `"jsonrpc":"2.0"` in the json output -// and check for it's presence in the input. +// and check for its presence in the input. // This is similar to the inverse of `#[serde(skip)]`, but serde doesn't provide // an attribute for this behavior so we need a manual ser/de implementation. impl Serialize for BrpRequest { @@ -1063,12 +1062,8 @@ impl Serialize for BrpRequest { let mut map = serializer.serialize_map(None)?; map.serialize_entry("jsonrpc", "2.0")?; map.serialize_entry("method", &self.method)?; - if self.id.is_some() { - map.serialize_entry("id", &self.id)?; - } - if self.params.is_some() { - map.serialize_entry("params", &self.params)?; - } + map.serialize_entry("id", &self.id)?; + map.serialize_entry("params", &self.params)?; map.end() } } @@ -1156,25 +1151,121 @@ impl<'de> Deserialize<'de> for BrpRequest { } /// A response according to BRP. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BrpResponse<'a> { - /// This field is mandatory and must be set to `"2.0"`. - pub jsonrpc: Cow<'a, str>, - +#[derive(Debug, Clone)] +pub struct BrpResponse { /// The id of the original request. pub id: Option, /// The actual response payload. - #[serde(flatten)] pub payload: BrpPayload, } -impl BrpResponse<'static> { +// BRP uses json-rpc 2.0, so we need to include `"jsonrpc":"2.0"` in the json output +// and check for its presence in the input. +impl Serialize for BrpResponse { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("jsonrpc", "2.0")?; + map.serialize_entry("id", &self.id)?; + match &self.payload { + BrpPayload::Result(value) => { + map.serialize_entry("result", value)?; + } + BrpPayload::Error(error) => { + map.serialize_entry("error", error)?; + } + } + map.end() + } +} + +impl<'de> Deserialize<'de> for BrpResponse { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + use serde::de; + + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "lowercase")] + enum Field { + JsonRpc, + Id, + Result, + Error, + } + + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = BrpResponse; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("struct BrpResponse") + } + + fn visit_map(self, mut map: V) -> Result + where + V: de::MapAccess<'de>, + { + let mut jsonrpc = false; + let mut id = None; + let mut payload = None; + while let Some(key) = map.next_key()? { + match key { + Field::JsonRpc => { + let value = map.next_value::()?; + if value != "2.0" { + return Err(de::Error::invalid_value( + de::Unexpected::Str(&value), + &"2.0", + )); + } + if jsonrpc { + return Err(de::Error::duplicate_field("jsonrpc")); + } + jsonrpc = true; + } + Field::Id => { + if id.is_some() { + return Err(de::Error::duplicate_field("payload")); + } + id = Some(map.next_value()?); + } + Field::Result => { + if payload.is_some() { + return Err(de::Error::duplicate_field("payload")); + } + payload = Some(BrpPayload::Result(map.next_value()?)); + } + Field::Error => { + if payload.is_some() { + return Err(de::Error::duplicate_field("payload")); + } + payload = Some(BrpPayload::Error(map.next_value()?)); + } + } + } + if !jsonrpc { + return Err(de::Error::missing_field("jsonrpc")); + } + let payload = payload.ok_or_else(|| de::Error::missing_field("payload"))?; + Ok(BrpResponse { id, payload }) + } + } + + deserializer.deserialize_map(Visitor) + } +} + +impl BrpResponse { /// Generates a [`BrpResponse`] from an id and a `Result`. #[must_use] pub fn new(id: Option, result: BrpResult) -> Self { Self { - jsonrpc: Cow::Borrowed("2.0"), id, payload: BrpPayload::from(result), } From 6f892e9ecfc83b471a1aa24e4bc7dde1a636a56b Mon Sep 17 00:00:00 2001 From: Marc Guiselin Date: Fri, 12 Jun 2026 17:23:51 +0200 Subject: [PATCH 3/5] Fix outdated docs --- crates/bevy_remote/src/lib.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 7e1f945224eb0..d3f9e9b8c4828 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -1026,13 +1026,15 @@ pub struct RemoteWatchingRequests(Vec<(BrpMessage, RemoteWatchingMethodSystemId) ///``` /// /// In Rust: -/// ```ignore -/// let req = BrpRequest { -/// jsonrpc: "2.0".to_string(), -/// method: BRP_LIST_METHOD.to_string(), // All the methods have consts -/// id: Some(ureq::json!(0)), -/// params: None, -/// }; +/// ``` +/// # use bevy_remote::builtin_methods::BRP_LIST_COMPONENTS_METHOD; +/// # use bevy_remote::BrpRequest; +/// # use serde_json::Value; +/// let req = BrpRequest { +/// method: BRP_LIST_COMPONENTS_METHOD.to_string(), // All the methods are consts +/// id: Some(Value::from(1)), +/// params: None, +/// }; /// ``` #[derive(Debug, Clone)] pub struct BrpRequest { From e18591e42e7a0f84f6995ace413d70747cca027e Mon Sep 17 00:00:00 2001 From: Marc Guiselin Date: Fri, 12 Jun 2026 17:55:20 +0200 Subject: [PATCH 4/5] Skip serializing None --- crates/bevy_remote/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index d3f9e9b8c4828..ea1732902e9ce 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -1064,8 +1064,12 @@ impl Serialize for BrpRequest { let mut map = serializer.serialize_map(None)?; map.serialize_entry("jsonrpc", "2.0")?; map.serialize_entry("method", &self.method)?; - map.serialize_entry("id", &self.id)?; - map.serialize_entry("params", &self.params)?; + if self.id.is_some() { + map.serialize_entry("id", &self.id)?; + } + if self.params.is_some() { + map.serialize_entry("params", &self.params)?; + } map.end() } } @@ -1171,7 +1175,9 @@ impl Serialize for BrpResponse { { let mut map = serializer.serialize_map(None)?; map.serialize_entry("jsonrpc", "2.0")?; - map.serialize_entry("id", &self.id)?; + if self.id.is_some() { + map.serialize_entry("id", &self.id)?; + } match &self.payload { BrpPayload::Result(value) => { map.serialize_entry("result", value)?; From 5aafeeae0485695ae412989f5579c95f4bbe0a31 Mon Sep 17 00:00:00 2001 From: Marc Date: Sat, 13 Jun 2026 10:07:27 +0000 Subject: [PATCH 5/5] Apply suggestion from @SpecificProtagonist Co-authored-by: Mira --- crates/bevy_remote/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index ea1732902e9ce..608c0029f6566 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -1239,7 +1239,7 @@ impl<'de> Deserialize<'de> for BrpResponse { } Field::Id => { if id.is_some() { - return Err(de::Error::duplicate_field("payload")); + return Err(de::Error::duplicate_field("id")); } id = Some(map.next_value()?); }