mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Use msgpack for API
This commit is contained in:
parent
3d328c1d42
commit
bda2af3dfb
12 changed files with 264 additions and 172 deletions
|
|
@ -163,6 +163,7 @@ impl serde::ser::Error for CompactJson5Error {
|
|||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the value cannot be serialized to JSON5.
|
||||
#[allow(dead_code)]
|
||||
pub fn to_string_compact<T: Serialize>(value: &T) -> anyhow::Result<String> {
|
||||
let mut output = String::new();
|
||||
let mut serializer = CompactJson5Serializer {
|
||||
|
|
|
|||
116
src/web/api.rs
116
src/web/api.rs
|
|
@ -19,7 +19,6 @@ use rbx_dom_weak::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
json,
|
||||
serve_session::ServeSession,
|
||||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||
web::{
|
||||
|
|
@ -28,12 +27,9 @@ use crate::{
|
|||
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
||||
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
||||
},
|
||||
util::{json_ok, json_response},
|
||||
},
|
||||
web_api::{
|
||||
BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest,
|
||||
SerializeResponse,
|
||||
util::{deserialize_msgpack, msgpack, msgpack_ok, serialize_msgpack},
|
||||
},
|
||||
web_api::{InstanceUpdate, RefPatchResponse, SerializeResponse},
|
||||
};
|
||||
|
||||
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
|
||||
|
|
@ -48,7 +44,7 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
|
|||
if is_upgrade_request(&request) {
|
||||
service.handle_api_socket(&mut request).await
|
||||
} else {
|
||||
json_response(
|
||||
msgpack(
|
||||
ErrorResponse::bad_request(
|
||||
"/api/socket must be called as a websocket upgrade request",
|
||||
),
|
||||
|
|
@ -56,15 +52,19 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
|
|||
)
|
||||
}
|
||||
}
|
||||
(&Method::POST, "/api/serialize") => service.handle_api_serialize(request).await,
|
||||
(&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await,
|
||||
(&Method::GET, path) if path.starts_with("/api/serialize/") => {
|
||||
service.handle_api_serialize(request).await
|
||||
}
|
||||
(&Method::GET, path) if path.starts_with("/api/ref-patch/") => {
|
||||
service.handle_api_ref_patch(request).await
|
||||
}
|
||||
|
||||
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
||||
service.handle_api_open(request).await
|
||||
}
|
||||
(&Method::POST, "/api/write") => service.handle_api_write(request).await,
|
||||
|
||||
(_method, path) => json_response(
|
||||
(_method, path) => msgpack(
|
||||
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||
StatusCode::NOT_FOUND,
|
||||
),
|
||||
|
|
@ -85,7 +85,7 @@ impl ApiService {
|
|||
let tree = self.serve_session.tree();
|
||||
let root_instance_id = tree.get_root_id();
|
||||
|
||||
json_ok(&ServerInfoResponse {
|
||||
msgpack_ok(&ServerInfoResponse {
|
||||
server_version: SERVER_VERSION.to_owned(),
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
session_id: self.serve_session.session_id(),
|
||||
|
|
@ -105,7 +105,7 @@ impl ApiService {
|
|||
let input_cursor: u32 = match argument.parse() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -116,7 +116,7 @@ impl ApiService {
|
|||
let (response, websocket) = match upgrade(request, None) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
|
|
@ -143,10 +143,10 @@ impl ApiService {
|
|||
|
||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||
|
||||
let request: WriteRequest = match json::from_slice(&body) {
|
||||
let request: WriteRequest = match deserialize_msgpack(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -154,7 +154,7 @@ impl ApiService {
|
|||
};
|
||||
|
||||
if request.session_id != session_id {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Wrong session ID"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -180,7 +180,7 @@ impl ApiService {
|
|||
})
|
||||
.unwrap();
|
||||
|
||||
json_ok(WriteResponse { session_id })
|
||||
msgpack_ok(WriteResponse { session_id })
|
||||
}
|
||||
|
||||
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||
|
|
@ -190,7 +190,7 @@ impl ApiService {
|
|||
let requested_ids = match requested_ids {
|
||||
Ok(ids) => ids,
|
||||
Err(_) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Malformed ID list"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -254,7 +254,7 @@ impl ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
json_ok(ReadResponse {
|
||||
msgpack_ok(ReadResponse {
|
||||
session_id: self.serve_session.session_id(),
|
||||
message_cursor,
|
||||
instances,
|
||||
|
|
@ -269,30 +269,23 @@ impl ApiService {
|
|||
/// that correspond to the requested Instances. These values have their
|
||||
/// `Value` property set to point to the requested Instance.
|
||||
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
|
||||
let session_id = self.serve_session.session_id();
|
||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||
let argument = &request.uri().path()["/api/serialize/".len()..];
|
||||
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
||||
|
||||
let request: SerializeRequest = match json::from_slice(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json_response(
|
||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||
let requested_ids = match requested_ids {
|
||||
Ok(ids) => ids,
|
||||
Err(_) => {
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Malformed ID list"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if request.session_id != session_id {
|
||||
return json_response(
|
||||
ErrorResponse::bad_request("Wrong session ID"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
|
||||
|
||||
let tree = self.serve_session.tree();
|
||||
for id in &request.ids {
|
||||
for id in &requested_ids {
|
||||
if let Some(instance) = tree.get_instance(*id) {
|
||||
let clone = response_dom.insert(
|
||||
Ref::none(),
|
||||
|
|
@ -316,7 +309,7 @@ impl ApiService {
|
|||
|
||||
response_dom.transfer_within(child_ref, object_value);
|
||||
} else {
|
||||
json_response(
|
||||
msgpack(
|
||||
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -327,9 +320,9 @@ impl ApiService {
|
|||
let mut source = Vec::new();
|
||||
rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
|
||||
|
||||
json_ok(SerializeResponse {
|
||||
msgpack_ok(SerializeResponse {
|
||||
session_id: self.serve_session.session_id(),
|
||||
model_contents: BufferEncode::new(source),
|
||||
model_contents: source,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -337,26 +330,22 @@ impl ApiService {
|
|||
/// provided IDs. Used because the plugin does not store a RojoTree,
|
||||
/// and referent properties need to be updated after the serialize
|
||||
/// endpoint is used.
|
||||
async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
|
||||
let session_id = self.serve_session.session_id();
|
||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
||||
async fn handle_api_ref_patch(&self, request: Request<Body>) -> Response<Body> {
|
||||
let argument = &request.uri().path()["/api/ref-patch/".len()..];
|
||||
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
||||
|
||||
let request: RefPatchRequest = match json::from_slice(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return json_response(
|
||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||
let requested_ids = match requested_ids {
|
||||
Ok(ids) => ids,
|
||||
Err(_) => {
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Malformed ID list"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if request.session_id != session_id {
|
||||
return json_response(
|
||||
ErrorResponse::bad_request("Wrong session ID"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
// Convert to HashSet for efficient lookup
|
||||
let ids: HashSet<Ref> = requested_ids.into_iter().collect();
|
||||
|
||||
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
|
||||
|
||||
|
|
@ -366,7 +355,7 @@ impl ApiService {
|
|||
let Variant::Ref(prop_value) = prop_value else {
|
||||
continue;
|
||||
};
|
||||
if let Some(target_id) = request.ids.get(prop_value) {
|
||||
if let Some(target_id) = ids.get(prop_value) {
|
||||
let instance_id = instance.id();
|
||||
let update =
|
||||
instance_updates
|
||||
|
|
@ -385,7 +374,7 @@ impl ApiService {
|
|||
}
|
||||
}
|
||||
|
||||
json_ok(RefPatchResponse {
|
||||
msgpack_ok(RefPatchResponse {
|
||||
session_id: self.serve_session.session_id(),
|
||||
patch: SubscribeMessage {
|
||||
added: HashMap::new(),
|
||||
|
|
@ -401,7 +390,7 @@ impl ApiService {
|
|||
let requested_id = match Ref::from_str(argument) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Invalid instance ID"),
|
||||
StatusCode::BAD_REQUEST,
|
||||
);
|
||||
|
|
@ -413,7 +402,7 @@ impl ApiService {
|
|||
let instance = match tree.get_instance(requested_id) {
|
||||
Some(instance) => instance,
|
||||
None => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request("Instance not found"),
|
||||
StatusCode::NOT_FOUND,
|
||||
);
|
||||
|
|
@ -423,7 +412,7 @@ impl ApiService {
|
|||
let script_path = match pick_script_path(instance) {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::bad_request(
|
||||
"No appropriate file could be found to open this script",
|
||||
),
|
||||
|
|
@ -436,7 +425,7 @@ impl ApiService {
|
|||
Ok(()) => {}
|
||||
Err(error) => match error {
|
||||
OpenError::Io(io_error) => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::internal_error(format!(
|
||||
"Attempting to open {} failed because of the following io error: {}",
|
||||
script_path.display(),
|
||||
|
|
@ -450,7 +439,7 @@ impl ApiService {
|
|||
status,
|
||||
stderr,
|
||||
} => {
|
||||
return json_response(
|
||||
return msgpack(
|
||||
ErrorResponse::internal_error(format!(
|
||||
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
||||
Error logs:
|
||||
|
|
@ -466,7 +455,7 @@ impl ApiService {
|
|||
},
|
||||
};
|
||||
|
||||
json_ok(OpenResponse {
|
||||
msgpack_ok(OpenResponse {
|
||||
session_id: self.serve_session.session_id(),
|
||||
})
|
||||
}
|
||||
|
|
@ -531,7 +520,7 @@ async fn handle_websocket_subscription(
|
|||
match result {
|
||||
Ok((new_cursor, messages)) => {
|
||||
if !messages.is_empty() {
|
||||
let json_message = {
|
||||
let msgpack_message = {
|
||||
let tree = tree_handle.lock().unwrap();
|
||||
let api_messages: Vec<_> = messages
|
||||
.into_iter()
|
||||
|
|
@ -555,13 +544,12 @@ async fn handle_websocket_subscription(
|
|||
}),
|
||||
};
|
||||
|
||||
// Use compact JSON5 encoder for transport efficiency, preserves NaN/Infinity
|
||||
json::to_string_compact(&response)?
|
||||
serialize_msgpack(response)?
|
||||
};
|
||||
|
||||
log::debug!("Sending batch of messages over WebSocket subscription");
|
||||
|
||||
if websocket.send(Message::Text(json_message)).await.is_err() {
|
||||
if websocket.send(Message::Binary(msgpack_message)).await.is_err() {
|
||||
// Client disconnected
|
||||
log::debug!("WebSocket subscription closed by client");
|
||||
break;
|
||||
|
|
@ -677,7 +665,7 @@ fn filter_subscribe_message_for_scripts(
|
|||
// ALL instances get ignoreUnknownInstances: true in scripts-only mode
|
||||
// This ensures we only sync script content, never delete/modify other instances
|
||||
// Non-scripts also have their properties cleared - they're just tree structure
|
||||
for (_id, inst) in &mut msg.added {
|
||||
for inst in msg.added.values_mut() {
|
||||
inst.metadata = Some(InstanceMetadata {
|
||||
ignore_unknown_instances: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -160,9 +160,33 @@ pub struct ServerInfoResponse {
|
|||
pub server_version: String,
|
||||
pub protocol_version: u64,
|
||||
pub project_name: String,
|
||||
#[serde(
|
||||
serialize_with = "serialize_place_ids",
|
||||
deserialize_with = "deserialize_place_ids",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
pub expected_place_ids: Option<HashSet<u64>>,
|
||||
#[serde(
|
||||
serialize_with = "serialize_place_ids",
|
||||
deserialize_with = "deserialize_place_ids",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
pub unexpected_place_ids: Option<HashSet<u64>>,
|
||||
#[serde(
|
||||
serialize_with = "serialize_optional_u64_as_f64",
|
||||
deserialize_with = "deserialize_optional_f64_as_u64",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
pub game_id: Option<u64>,
|
||||
#[serde(
|
||||
serialize_with = "serialize_optional_u64_as_f64",
|
||||
deserialize_with = "deserialize_optional_f64_as_u64",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default
|
||||
)]
|
||||
pub place_id: Option<u64>,
|
||||
pub root_instance_id: Ref,
|
||||
/// When true, the server only supports syncing script Source property changes.
|
||||
|
|
@ -171,6 +195,50 @@ pub struct ServerInfoResponse {
|
|||
pub sync_source_only: bool,
|
||||
}
|
||||
|
||||
// Serialize place IDs as f64 to avoid msgpack uint64 encoding issues with Lua
|
||||
fn serialize_place_ids<S>(ids: &Option<HashSet<u64>>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match ids {
|
||||
Some(set) => {
|
||||
use serde::ser::SerializeSeq;
|
||||
let mut seq = serializer.serialize_seq(Some(set.len()))?;
|
||||
for id in set {
|
||||
seq.serialize_element(&(*id as f64))?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_place_ids<'de, D>(deserializer: D) -> Result<Option<HashSet<u64>>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<Vec<f64>> = Option::deserialize(deserializer)?;
|
||||
Ok(opt.map(|v| v.into_iter().map(|f| f as u64).collect()))
|
||||
}
|
||||
|
||||
fn serialize_optional_u64_as_f64<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match value {
|
||||
Some(v) => serializer.serialize_f64(*v as f64),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_optional_f64_as_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<f64> = Option::deserialize(deserializer)?;
|
||||
Ok(opt.map(|f| f as u64))
|
||||
}
|
||||
|
||||
/// Response body from /api/read/{id}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -253,31 +321,8 @@ pub struct SerializeRequest {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SerializeResponse {
|
||||
pub session_id: SessionId,
|
||||
pub model_contents: BufferEncode,
|
||||
}
|
||||
|
||||
/// Using this struct we can force Roblox to JSONDecode this as a buffer.
|
||||
/// This is what Roblox's serde APIs use, so it saves a step in the plugin.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BufferEncode {
|
||||
m: (),
|
||||
t: Cow<'static, str>,
|
||||
base64: String,
|
||||
}
|
||||
|
||||
impl BufferEncode {
|
||||
pub fn new(content: Vec<u8>) -> Self {
|
||||
let base64 = data_encoding::BASE64.encode(&content);
|
||||
Self {
|
||||
m: (),
|
||||
t: Cow::Borrowed("buffer"),
|
||||
base64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model(&self) -> &str {
|
||||
&self.base64
|
||||
}
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub model_contents: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,52 @@
|
|||
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::json as json_module;
|
||||
|
||||
pub fn json_ok<T: Serialize>(value: T) -> Response<Body> {
|
||||
json_response(value, StatusCode::OK)
|
||||
pub fn msgpack_ok<T: Serialize>(value: T) -> Response<Body> {
|
||||
msgpack(value, StatusCode::OK)
|
||||
}
|
||||
|
||||
/// Alias for backwards compatibility
|
||||
pub use json_response as json;
|
||||
pub fn msgpack<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
let mut serialized = Vec::new();
|
||||
let mut serializer = rmp_serde::Serializer::new(&mut serialized)
|
||||
.with_human_readable()
|
||||
.with_struct_map();
|
||||
|
||||
pub fn json_response<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
// Use compact JSON5 for transport efficiency, preserves NaN/Infinity
|
||||
let serialized = match json_module::to_string_compact(&value) {
|
||||
if let Err(err) = value.serialize(&mut serializer) {
|
||||
return Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header(CONTENT_TYPE, "text/plain")
|
||||
.body(Body::from(err.to_string()))
|
||||
.unwrap();
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(code)
|
||||
.header(CONTENT_TYPE, "application/msgpack")
|
||||
.body(Body::from(serialized))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn serialize_msgpack<T: Serialize>(value: T) -> anyhow::Result<Vec<u8>> {
|
||||
let mut serialized = Vec::new();
|
||||
let mut serializer = rmp_serde::Serializer::new(&mut serialized)
|
||||
.with_human_readable()
|
||||
.with_struct_map();
|
||||
|
||||
value.serialize(&mut serializer)?;
|
||||
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
pub fn deserialize_msgpack<'a, T: Deserialize<'a>>(
|
||||
input: &'a [u8],
|
||||
) -> Result<T, rmp_serde::decode::Error> {
|
||||
let mut deserializer = rmp_serde::Deserializer::new(input).with_human_readable();
|
||||
|
||||
T::deserialize(&mut deserializer)
|
||||
}
|
||||
|
||||
pub fn json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||
let serialized = match serde_json::to_string(&value) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
return Response::builder()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue