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
38
Cargo.lock
generated
38
Cargo.lock
generated
|
|
@ -200,9 +200,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.53"
|
version = "1.2.54"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
|
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
|
|
@ -2214,10 +2214,12 @@ dependencies = [
|
||||||
"rbx_xml",
|
"rbx_xml",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ritz",
|
"ritz",
|
||||||
|
"rmp-serde",
|
||||||
"roblox_install",
|
"roblox_install",
|
||||||
"rojo-insta-ext",
|
"rojo-insta-ext",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_bytes",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"strum",
|
"strum",
|
||||||
|
|
@ -2358,6 +2360,16 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_bytes"
|
||||||
|
version = "0.11.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_cbor"
|
name = "serde_cbor"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
|
@ -2501,9 +2513,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
|
|
@ -2698,7 +2710,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"mio 1.1.1",
|
"mio 1.1.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.1",
|
"socket2 0.6.2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
@ -2940,9 +2952,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|
@ -3487,18 +3499,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.33"
|
version = "0.8.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.33"
|
version = "0.8.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.106",
|
"proc-macro2 1.0.106",
|
||||||
"quote 1.0.44",
|
"quote 1.0.44",
|
||||||
|
|
@ -3561,9 +3573,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.16"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
|
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ toml = "0.5.11"
|
||||||
termcolor = "1.4.1"
|
termcolor = "1.4.1"
|
||||||
thiserror = "1.0.69"
|
thiserror = "1.0.69"
|
||||||
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros"] }
|
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
uuid = { version = "1.19.0", features = ["v4", "serde"] }
|
uuid = { version = "1.20.0", features = ["v4", "serde"] }
|
||||||
clap = { version = "3.2.25", features = ["derive"] }
|
clap = { version = "3.2.25", features = ["derive"] }
|
||||||
profiling = "1.0.17"
|
profiling = "1.0.17"
|
||||||
yaml-rust2 = "0.10.4"
|
yaml-rust2 = "0.10.4"
|
||||||
|
|
@ -96,6 +96,8 @@ data-encoding = "2.10.0"
|
||||||
blake3 = "1.8.3"
|
blake3 = "1.8.3"
|
||||||
float-cmp = "0.9.0"
|
float-cmp = "0.9.0"
|
||||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||||
|
rmp-serde = "1.3.1"
|
||||||
|
serde_bytes = "0.11.19"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|
|
||||||
15
build.rs
15
build.rs
|
|
@ -30,6 +30,20 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip binary asset files that can't be read as text
|
||||||
|
if file_name.ends_with(".png")
|
||||||
|
|| file_name.ends_with(".jpg")
|
||||||
|
|| file_name.ends_with(".gif")
|
||||||
|
|| file_name.ends_with(".webp")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip directories that aren't needed for plugin functionality
|
||||||
|
if file_name == "assets" || file_name == "benchmark" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
let child_snapshot = snapshot_from_fs_path(&entry.path())?;
|
||||||
children.push((file_name, child_snapshot));
|
children.push((file_name, child_snapshot));
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +80,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||||
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?,
|
||||||
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?,
|
||||||
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?,
|
||||||
|
"msgpack" => snapshot_from_fs_path(&plugin_dir.join("msgpack.lua"))?,
|
||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
|
||||||
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
local JSON5Decoder = require(script.Parent.Parent.json5.JSON5Decoder)
|
|
||||||
|
local msgpack = require(script.Parent.Parent.msgpack)
|
||||||
|
|
||||||
local stringTemplate = [[
|
local stringTemplate = [[
|
||||||
Http.Response {
|
Http.Response {
|
||||||
|
|
@ -29,13 +30,11 @@ function Response:isSuccess()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Response:json()
|
function Response:json()
|
||||||
-- Try fast native decode first, fallback to JSON5 for NaN/Infinity
|
return HttpService:JSONDecode(self.body)
|
||||||
local success, result = pcall(HttpService.JSONDecode, HttpService, self.body)
|
end
|
||||||
if success then
|
|
||||||
return result
|
function Response:msgpack()
|
||||||
end
|
return msgpack.decode(self.body)
|
||||||
print("[HTTP Response:json()] Native decode failed, using JSON5 for size:", #self.body)
|
|
||||||
return JSON5Decoder.Decode(self.body)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Response
|
return Response
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Promise = require(script.Parent.Promise)
|
|
||||||
local Log = require(script.Parent.Log)
|
local Log = require(script.Parent.Log)
|
||||||
local JSON5Decoder = require(script.Parent.json5.JSON5Decoder)
|
local msgpack = require(script.Parent.msgpack)
|
||||||
local JSONEncoder = require(script.Parent.json5.JSONEncoder)
|
local Promise = require(script.Parent.Promise)
|
||||||
|
|
||||||
local HttpError = require(script.Error)
|
local HttpError = require(script.Error)
|
||||||
local HttpResponse = require(script.Response)
|
local HttpResponse = require(script.Response)
|
||||||
|
|
@ -63,17 +62,19 @@ function Http.post(url, body)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Http.jsonEncode(object)
|
function Http.jsonEncode(object)
|
||||||
return JSONEncoder.Compact5(object)
|
return HttpService:JSONEncode(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Http.jsonDecode(source)
|
function Http.jsonDecode(source)
|
||||||
-- Try fast native decode first, fallback to JSON5 for NaN/Infinity
|
return HttpService:JSONDecode(source)
|
||||||
local success, result = pcall(HttpService.JSONDecode, HttpService, source)
|
end
|
||||||
if success then
|
|
||||||
return result
|
function Http.msgpackEncode(object)
|
||||||
end
|
return msgpack.encode(object)
|
||||||
print("[Http.jsonDecode] Native decode failed, using JSON5 for size:", #source)
|
end
|
||||||
return JSON5Decoder.Decode(source)
|
|
||||||
|
function Http.msgpackDecode(source)
|
||||||
|
return msgpack.decode(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
return Http
|
return Http
|
||||||
|
|
|
||||||
2
plugin/msgpack.lua
Normal file
2
plugin/msgpack.lua
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Module alias for msgpack-luau
|
||||||
|
return require(script.Parent.Packages["msgpack-luau"].src.msgpack)
|
||||||
|
|
@ -145,7 +145,7 @@ function ApiContext:connect()
|
||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(Http.Response.json)
|
:andThen(Http.Response.msgpack)
|
||||||
:andThen(rejectWrongProtocolVersion)
|
:andThen(rejectWrongProtocolVersion)
|
||||||
:andThen(function(body)
|
:andThen(function(body)
|
||||||
assert(validateApiInfo(body))
|
assert(validateApiInfo(body))
|
||||||
|
|
@ -163,7 +163,7 @@ end
|
||||||
function ApiContext:read(ids)
|
function ApiContext:read(ids)
|
||||||
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
|
|
||||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
@ -191,9 +191,9 @@ function ApiContext:write(patch)
|
||||||
table.insert(updated, fixedUpdate)
|
table.insert(updated, fixedUpdate)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only add the 'added' field if the table is non-empty, or else Roblox's
|
-- Only add the 'added' field if the table is non-empty, or else the msgpack
|
||||||
-- JSON implementation will turn the table into an array instead of an
|
-- encode implementation will turn the table into an array instead of a map,
|
||||||
-- object, causing API validation to fail.
|
-- causing API validation to fail.
|
||||||
local added
|
local added
|
||||||
if next(patch.added) ~= nil then
|
if next(patch.added) ~= nil then
|
||||||
added = patch.added
|
added = patch.added
|
||||||
|
|
@ -206,9 +206,12 @@ function ApiContext:write(patch)
|
||||||
added = added,
|
added = added,
|
||||||
}
|
}
|
||||||
|
|
||||||
body = Http.jsonEncode(body)
|
body = Http.msgpackEncode(body)
|
||||||
|
|
||||||
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
|
return Http.post(url, body)
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.msgpack)
|
||||||
|
:andThen(function(responseBody)
|
||||||
Log.info("Write response: {:?}", responseBody)
|
Log.info("Write response: {:?}", responseBody)
|
||||||
|
|
||||||
return responseBody
|
return responseBody
|
||||||
|
|
@ -234,7 +237,7 @@ function ApiContext:connectWebSocket(packetHandlers)
|
||||||
local closed, errored, received
|
local closed, errored, received
|
||||||
|
|
||||||
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
||||||
local data = Http.jsonDecode(msg)
|
local data = Http.msgpackDecode(msg)
|
||||||
if data.sessionId ~= self.__sessionId then
|
if data.sessionId ~= self.__sessionId then
|
||||||
Log.warn("Received message with wrong session ID; ignoring")
|
Log.warn("Received message with wrong session ID; ignoring")
|
||||||
return
|
return
|
||||||
|
|
@ -280,7 +283,7 @@ end
|
||||||
function ApiContext:open(id)
|
function ApiContext:open(id)
|
||||||
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
@ -290,38 +293,30 @@ function ApiContext:open(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:serialize(ids: { string })
|
function ApiContext:serialize(ids: { string })
|
||||||
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
|
||||||
|
|
||||||
return Http.post(url, request_body)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
if body.sessionId ~= self.__sessionId then
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(response_body)
|
|
||||||
if response_body.sessionId ~= self.__sessionId then
|
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(validateApiSerialize(response_body))
|
assert(validateApiSerialize(body))
|
||||||
|
|
||||||
return response_body
|
return body
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:refPatch(ids: { string })
|
function ApiContext:refPatch(ids: { string })
|
||||||
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
|
||||||
|
|
||||||
return Http.post(url, request_body)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
if body.sessionId ~= self.__sessionId then
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(response_body)
|
|
||||||
if response_body.sessionId ~= self.__sessionId then
|
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(validateApiRefPatch(response_body))
|
assert(validateApiRefPatch(body))
|
||||||
|
|
||||||
return response_body
|
return body
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ impl serde::ser::Error for CompactJson5Error {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the value cannot be serialized to JSON5.
|
/// 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> {
|
pub fn to_string_compact<T: Serialize>(value: &T) -> anyhow::Result<String> {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut serializer = CompactJson5Serializer {
|
let mut serializer = CompactJson5Serializer {
|
||||||
|
|
|
||||||
116
src/web/api.rs
116
src/web/api.rs
|
|
@ -19,7 +19,6 @@ use rbx_dom_weak::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
json,
|
|
||||||
serve_session::ServeSession,
|
serve_session::ServeSession,
|
||||||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||||
web::{
|
web::{
|
||||||
|
|
@ -28,12 +27,9 @@ use crate::{
|
||||||
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
||||||
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
||||||
},
|
},
|
||||||
util::{json_ok, json_response},
|
util::{deserialize_msgpack, msgpack, msgpack_ok, serialize_msgpack},
|
||||||
},
|
|
||||||
web_api::{
|
|
||||||
BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest,
|
|
||||||
SerializeResponse,
|
|
||||||
},
|
},
|
||||||
|
web_api::{InstanceUpdate, RefPatchResponse, SerializeResponse},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
|
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) {
|
if is_upgrade_request(&request) {
|
||||||
service.handle_api_socket(&mut request).await
|
service.handle_api_socket(&mut request).await
|
||||||
} else {
|
} else {
|
||||||
json_response(
|
msgpack(
|
||||||
ErrorResponse::bad_request(
|
ErrorResponse::bad_request(
|
||||||
"/api/socket must be called as a websocket upgrade 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::GET, path) if path.starts_with("/api/serialize/") => {
|
||||||
(&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await,
|
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/") => {
|
(&Method::POST, path) if path.starts_with("/api/open/") => {
|
||||||
service.handle_api_open(request).await
|
service.handle_api_open(request).await
|
||||||
}
|
}
|
||||||
(&Method::POST, "/api/write") => service.handle_api_write(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)),
|
ErrorResponse::not_found(format!("Route not found: {}", path)),
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
),
|
),
|
||||||
|
|
@ -85,7 +85,7 @@ impl ApiService {
|
||||||
let tree = self.serve_session.tree();
|
let tree = self.serve_session.tree();
|
||||||
let root_instance_id = tree.get_root_id();
|
let root_instance_id = tree.get_root_id();
|
||||||
|
|
||||||
json_ok(&ServerInfoResponse {
|
msgpack_ok(&ServerInfoResponse {
|
||||||
server_version: SERVER_VERSION.to_owned(),
|
server_version: SERVER_VERSION.to_owned(),
|
||||||
protocol_version: PROTOCOL_VERSION,
|
protocol_version: PROTOCOL_VERSION,
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
|
|
@ -105,7 +105,7 @@ impl ApiService {
|
||||||
let input_cursor: u32 = match argument.parse() {
|
let input_cursor: u32 = match argument.parse() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
|
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -116,7 +116,7 @@ impl ApiService {
|
||||||
let (response, websocket) = match upgrade(request, None) {
|
let (response, websocket) = match upgrade(request, None) {
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
|
|
@ -143,10 +143,10 @@ impl ApiService {
|
||||||
|
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
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,
|
Ok(request) => request,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -154,7 +154,7 @@ impl ApiService {
|
||||||
};
|
};
|
||||||
|
|
||||||
if request.session_id != session_id {
|
if request.session_id != session_id {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Wrong session ID"),
|
ErrorResponse::bad_request("Wrong session ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -180,7 +180,7 @@ impl ApiService {
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
json_ok(WriteResponse { session_id })
|
msgpack_ok(WriteResponse { session_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
|
||||||
|
|
@ -190,7 +190,7 @@ impl ApiService {
|
||||||
let requested_ids = match requested_ids {
|
let requested_ids = match requested_ids {
|
||||||
Ok(ids) => ids,
|
Ok(ids) => ids,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Malformed ID list"),
|
ErrorResponse::bad_request("Malformed ID list"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -254,7 +254,7 @@ impl ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_ok(ReadResponse {
|
msgpack_ok(ReadResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
message_cursor,
|
message_cursor,
|
||||||
instances,
|
instances,
|
||||||
|
|
@ -269,30 +269,23 @@ impl ApiService {
|
||||||
/// that correspond to the requested Instances. These values have their
|
/// that correspond to the requested Instances. These values have their
|
||||||
/// `Value` property set to point to the requested Instance.
|
/// `Value` property set to point to the requested Instance.
|
||||||
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
|
||||||
let session_id = self.serve_session.session_id();
|
let argument = &request.uri().path()["/api/serialize/".len()..];
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
||||||
|
|
||||||
let request: SerializeRequest = match json::from_slice(&body) {
|
let requested_ids = match requested_ids {
|
||||||
Ok(request) => request,
|
Ok(ids) => ids,
|
||||||
Err(err) => {
|
Err(_) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request("Malformed ID list"),
|
||||||
StatusCode::BAD_REQUEST,
|
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 mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
|
||||||
|
|
||||||
let tree = self.serve_session.tree();
|
let tree = self.serve_session.tree();
|
||||||
for id in &request.ids {
|
for id in &requested_ids {
|
||||||
if let Some(instance) = tree.get_instance(*id) {
|
if let Some(instance) = tree.get_instance(*id) {
|
||||||
let clone = response_dom.insert(
|
let clone = response_dom.insert(
|
||||||
Ref::none(),
|
Ref::none(),
|
||||||
|
|
@ -316,7 +309,7 @@ impl ApiService {
|
||||||
|
|
||||||
response_dom.transfer_within(child_ref, object_value);
|
response_dom.transfer_within(child_ref, object_value);
|
||||||
} else {
|
} else {
|
||||||
json_response(
|
msgpack(
|
||||||
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
|
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -327,9 +320,9 @@ impl ApiService {
|
||||||
let mut source = Vec::new();
|
let mut source = Vec::new();
|
||||||
rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
|
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(),
|
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,
|
/// provided IDs. Used because the plugin does not store a RojoTree,
|
||||||
/// and referent properties need to be updated after the serialize
|
/// and referent properties need to be updated after the serialize
|
||||||
/// endpoint is used.
|
/// endpoint is used.
|
||||||
async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
|
async fn handle_api_ref_patch(&self, request: Request<Body>) -> Response<Body> {
|
||||||
let session_id = self.serve_session.session_id();
|
let argument = &request.uri().path()["/api/ref-patch/".len()..];
|
||||||
let body = body::to_bytes(request.into_body()).await.unwrap();
|
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
|
||||||
|
|
||||||
let request: RefPatchRequest = match json::from_slice(&body) {
|
let requested_ids = match requested_ids {
|
||||||
Ok(request) => request,
|
Ok(ids) => ids,
|
||||||
Err(err) => {
|
Err(_) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
|
ErrorResponse::bad_request("Malformed ID list"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if request.session_id != session_id {
|
// Convert to HashSet for efficient lookup
|
||||||
return json_response(
|
let ids: HashSet<Ref> = requested_ids.into_iter().collect();
|
||||||
ErrorResponse::bad_request("Wrong session ID"),
|
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
|
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
|
||||||
|
|
||||||
|
|
@ -366,7 +355,7 @@ impl ApiService {
|
||||||
let Variant::Ref(prop_value) = prop_value else {
|
let Variant::Ref(prop_value) = prop_value else {
|
||||||
continue;
|
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 instance_id = instance.id();
|
||||||
let update =
|
let update =
|
||||||
instance_updates
|
instance_updates
|
||||||
|
|
@ -385,7 +374,7 @@ impl ApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
json_ok(RefPatchResponse {
|
msgpack_ok(RefPatchResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
patch: SubscribeMessage {
|
patch: SubscribeMessage {
|
||||||
added: HashMap::new(),
|
added: HashMap::new(),
|
||||||
|
|
@ -401,7 +390,7 @@ impl ApiService {
|
||||||
let requested_id = match Ref::from_str(argument) {
|
let requested_id = match Ref::from_str(argument) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Invalid instance ID"),
|
ErrorResponse::bad_request("Invalid instance ID"),
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
);
|
);
|
||||||
|
|
@ -413,7 +402,7 @@ impl ApiService {
|
||||||
let instance = match tree.get_instance(requested_id) {
|
let instance = match tree.get_instance(requested_id) {
|
||||||
Some(instance) => instance,
|
Some(instance) => instance,
|
||||||
None => {
|
None => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request("Instance not found"),
|
ErrorResponse::bad_request("Instance not found"),
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
);
|
);
|
||||||
|
|
@ -423,7 +412,7 @@ impl ApiService {
|
||||||
let script_path = match pick_script_path(instance) {
|
let script_path = match pick_script_path(instance) {
|
||||||
Some(path) => path,
|
Some(path) => path,
|
||||||
None => {
|
None => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::bad_request(
|
ErrorResponse::bad_request(
|
||||||
"No appropriate file could be found to open this script",
|
"No appropriate file could be found to open this script",
|
||||||
),
|
),
|
||||||
|
|
@ -436,7 +425,7 @@ impl ApiService {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(error) => match error {
|
Err(error) => match error {
|
||||||
OpenError::Io(io_error) => {
|
OpenError::Io(io_error) => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!(
|
ErrorResponse::internal_error(format!(
|
||||||
"Attempting to open {} failed because of the following io error: {}",
|
"Attempting to open {} failed because of the following io error: {}",
|
||||||
script_path.display(),
|
script_path.display(),
|
||||||
|
|
@ -450,7 +439,7 @@ impl ApiService {
|
||||||
status,
|
status,
|
||||||
stderr,
|
stderr,
|
||||||
} => {
|
} => {
|
||||||
return json_response(
|
return msgpack(
|
||||||
ErrorResponse::internal_error(format!(
|
ErrorResponse::internal_error(format!(
|
||||||
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
||||||
Error logs:
|
Error logs:
|
||||||
|
|
@ -466,7 +455,7 @@ impl ApiService {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
json_ok(OpenResponse {
|
msgpack_ok(OpenResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -531,7 +520,7 @@ async fn handle_websocket_subscription(
|
||||||
match result {
|
match result {
|
||||||
Ok((new_cursor, messages)) => {
|
Ok((new_cursor, messages)) => {
|
||||||
if !messages.is_empty() {
|
if !messages.is_empty() {
|
||||||
let json_message = {
|
let msgpack_message = {
|
||||||
let tree = tree_handle.lock().unwrap();
|
let tree = tree_handle.lock().unwrap();
|
||||||
let api_messages: Vec<_> = messages
|
let api_messages: Vec<_> = messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -555,13 +544,12 @@ async fn handle_websocket_subscription(
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use compact JSON5 encoder for transport efficiency, preserves NaN/Infinity
|
serialize_msgpack(response)?
|
||||||
json::to_string_compact(&response)?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
log::debug!("Sending batch of messages over WebSocket subscription");
|
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
|
// Client disconnected
|
||||||
log::debug!("WebSocket subscription closed by client");
|
log::debug!("WebSocket subscription closed by client");
|
||||||
break;
|
break;
|
||||||
|
|
@ -677,7 +665,7 @@ fn filter_subscribe_message_for_scripts(
|
||||||
// ALL instances get ignoreUnknownInstances: true in scripts-only mode
|
// ALL instances get ignoreUnknownInstances: true in scripts-only mode
|
||||||
// This ensures we only sync script content, never delete/modify other instances
|
// This ensures we only sync script content, never delete/modify other instances
|
||||||
// Non-scripts also have their properties cleared - they're just tree structure
|
// 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 {
|
inst.metadata = Some(InstanceMetadata {
|
||||||
ignore_unknown_instances: true,
|
ignore_unknown_instances: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,33 @@ pub struct ServerInfoResponse {
|
||||||
pub server_version: String,
|
pub server_version: String,
|
||||||
pub protocol_version: u64,
|
pub protocol_version: u64,
|
||||||
pub project_name: String,
|
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>>,
|
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>>,
|
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>,
|
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 place_id: Option<u64>,
|
||||||
pub root_instance_id: Ref,
|
pub root_instance_id: Ref,
|
||||||
/// When true, the server only supports syncing script Source property changes.
|
/// When true, the server only supports syncing script Source property changes.
|
||||||
|
|
@ -171,6 +195,50 @@ pub struct ServerInfoResponse {
|
||||||
pub sync_source_only: bool,
|
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}
|
/// Response body from /api/read/{id}
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
@ -253,31 +321,8 @@ pub struct SerializeRequest {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SerializeResponse {
|
pub struct SerializeResponse {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
pub model_contents: BufferEncode,
|
#[serde(with = "serde_bytes")]
|
||||||
}
|
pub model_contents: Vec<u8>,
|
||||||
|
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,52 @@
|
||||||
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::json as json_module;
|
pub fn msgpack_ok<T: Serialize>(value: T) -> Response<Body> {
|
||||||
|
msgpack(value, StatusCode::OK)
|
||||||
pub fn json_ok<T: Serialize>(value: T) -> Response<Body> {
|
|
||||||
json_response(value, StatusCode::OK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Alias for backwards compatibility
|
pub fn msgpack<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
|
||||||
pub use json_response as json;
|
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> {
|
if let Err(err) = value.serialize(&mut serializer) {
|
||||||
// Use compact JSON5 for transport efficiency, preserves NaN/Infinity
|
return Response::builder()
|
||||||
let serialized = match json_module::to_string_compact(&value) {
|
.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,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Response::builder()
|
return Response::builder()
|
||||||
|
|
|
||||||
|
|
@ -646,7 +646,7 @@ fn meshpart_with_id() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let serialize_response = session
|
let serialize_response = session
|
||||||
.get_api_serialize(&[*meshpart, *objectvalue], info.session_id)
|
.get_api_serialize(&[*meshpart, *objectvalue])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// We don't assert a snapshot on the SerializeResponse because the model includes the
|
// We don't assert a snapshot on the SerializeResponse because the model includes the
|
||||||
|
|
@ -673,9 +673,7 @@ fn forced_parent() {
|
||||||
read_response.intern_and_redact(&mut redactions, root_id)
|
read_response.intern_and_redact(&mut redactions, root_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
let serialize_response = session
|
let serialize_response = session.get_api_serialize(&[root_id]).unwrap();
|
||||||
.get_api_serialize(&[root_id], info.session_id)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(serialize_response.session_id, info.session_id);
|
assert_eq!(serialize_response.session_id, info.session_id);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue