rojo/src/web/interface.rs
Joe 50b9e18514 Refactor and rebrand Rojo to Atlas
- Renamed the project from Rojo to Atlas, updating all relevant files, scripts, and documentation to reflect the new branding.
- Bumped version to 7.7.0-rc.2 and updated protocol version from 5 to 6.
- Modified installation scripts and CI workflows to use the new binary name and paths.
- Adjusted plugin settings and internal identifiers to prevent conflicts with the original Rojo.
- Enhanced session management and API response handling to support the new server fork identifier.
- Updated project templates and README files to guide users on using Atlas.
2026-02-07 13:21:52 -06:00

408 lines
12 KiB
Rust

//! Defines all the structs needed to interact with the Rojo Serve API. This is
//! useful for tests to be able to use the same data structures as the
//! implementation.
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
};
use rbx_dom_weak::{
types::{Ref, Variant, VariantType},
Ustr, UstrMap,
};
use serde::{Deserialize, Serialize};
use strum::Display;
use crate::{
session_id::SessionId,
snapshot::{
AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
},
};
/// Server version to report over the API, not exposed outside this crate.
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Current protocol version, which is required to match.
pub const PROTOCOL_VERSION: u64 = 6;
/// Message returned by Rojo API when a change has occurred.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeMessage<'a> {
pub removed: Vec<Ref>,
pub added: HashMap<Ref, Instance<'a>>,
pub updated: Vec<InstanceUpdate>,
}
impl<'a> SubscribeMessage<'a> {
pub(crate) fn from_patch_update(tree: &'a RojoTree, patch: AppliedPatchSet) -> Self {
let removed = patch.removed;
let mut added = HashMap::new();
for id in patch.added {
let instance = tree.get_instance(id).unwrap();
added.insert(id, Instance::from_rojo_instance(instance));
for instance in tree.descendants(id) {
added.insert(instance.id(), Instance::from_rojo_instance(instance));
}
}
let updated = patch
.updated
.into_iter()
.map(|update| {
let changed_metadata = update
.changed_metadata
.as_ref()
.map(InstanceMetadata::from_rojo_metadata);
let changed_properties = update
.changed_properties
.into_iter()
.filter(|(_key, value)| property_filter(value.as_ref()))
.collect();
InstanceUpdate {
id: update.id,
changed_name: update.changed_name,
changed_class_name: update.changed_class_name,
changed_properties,
changed_metadata,
}
})
.collect();
Self {
removed,
added,
updated,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceUpdate {
pub id: Ref,
pub changed_name: Option<String>,
pub changed_class_name: Option<Ustr>,
// TODO: Transform from UstrMap<String, Option<_>> to something else, since
// null will get lost when decoding from JSON in some languages.
#[serde(default)]
pub changed_properties: UstrMap<Option<Variant>>,
pub changed_metadata: Option<InstanceMetadata>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstanceMetadata {
pub ignore_unknown_instances: bool,
}
impl InstanceMetadata {
pub(crate) fn from_rojo_metadata(meta: &RojoInstanceMetadata) -> Self {
Self {
ignore_unknown_instances: meta.ignore_unknown_instances,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Instance<'a> {
pub id: Ref,
pub parent: Ref,
pub name: Cow<'a, str>,
pub class_name: Ustr,
pub properties: UstrMap<Cow<'a, Variant>>,
pub children: Cow<'a, [Ref]>,
pub metadata: Option<InstanceMetadata>,
}
impl Instance<'_> {
pub(crate) fn from_rojo_instance(source: InstanceWithMeta<'_>) -> Instance<'_> {
let properties = source
.properties()
.iter()
.filter(|(_key, value)| property_filter(Some(value)))
.map(|(key, value)| (*key, Cow::Borrowed(value)))
.collect();
Instance {
id: source.id(),
parent: source.parent(),
name: Cow::Borrowed(source.name()),
class_name: source.class_name(),
properties,
children: Cow::Borrowed(source.children()),
metadata: Some(InstanceMetadata::from_rojo_metadata(source.metadata())),
}
}
}
fn property_filter(value: Option<&Variant>) -> bool {
let ty = value.map(|value| value.ty());
// Lua can't do anything with SharedString values. They also can't be
// serialized directly by Serde!
ty != Some(VariantType::SharedString)
}
/// Response body from /api/rojo
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerInfoResponse {
pub session_id: SessionId,
pub server_version: String,
pub protocol_version: u64,
pub server_fork: 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>>,
#[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.
/// Other property changes will be ignored.
#[serde(default)]
pub sync_source_only: bool,
/// When true, hidden/internal services should be ignored during sync.
/// The plugin should not mark these services as "to delete" during forward sync.
#[serde(default)]
pub ignore_hidden_services: bool,
/// List of service names that are considered "visible" when ignoreHiddenServices is true.
/// Services not in this list should be ignored during sync operations.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub visible_services: Vec<String>,
}
// 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")]
pub struct ReadResponse<'a> {
pub session_id: SessionId,
pub message_cursor: u32,
pub instances: HashMap<Ref, Instance<'a>>,
}
/// Instance data for creating new instances via the write API
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddedInstance {
/// The parent instance's Ref (must exist in Rojo's tree)
/// For top-level added instances, this is required.
/// For nested children, this can be omitted (parent is implied by nesting).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<Ref>,
/// The instance name
pub name: String,
/// The class name (e.g., "ModuleScript", "Script", "LocalScript")
pub class_name: String,
/// Properties to set on the instance
#[serde(default)]
pub properties: HashMap<String, Variant>,
/// Children instances (recursively encoded for hierarchical syncback)
#[serde(default)]
pub children: Vec<AddedInstance>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteRequest {
pub session_id: SessionId,
pub removed: Vec<Ref>,
#[serde(default)]
pub added: HashMap<Ref, AddedInstance>,
pub updated: Vec<InstanceUpdate>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WriteResponse {
pub session_id: SessionId,
}
/// Packet type enum for different websocket message types
#[derive(Debug, Serialize, Deserialize, Display, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
#[strum(serialize_all = "camelCase")]
pub enum SocketPacketType {
Messages,
// TODO: Can we cleanly use the socket for all communication?
// Serialize,
// RefPatch,
}
/// Body content for messages packet type
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessagesPacket<'a> {
pub message_cursor: u32,
pub messages: Vec<SubscribeMessage<'a>>,
}
/// Body content for different packet types
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SocketPacketBody<'a> {
Messages(MessagesPacket<'a>),
// TODO: Can we cleanly use the socket for all communication?
// Serialize(SerializePacket),
// RefPatch(RefPatchPacket<'a>),
}
/// Message content from /api/socket
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SocketPacket<'a> {
pub session_id: SessionId,
pub packet_type: SocketPacketType,
pub body: SocketPacketBody<'a>,
}
/// Response body from /api/open/{id}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenResponse {
pub session_id: SessionId,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SerializeRequest {
pub session_id: SessionId,
pub ids: Vec<Ref>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SerializeResponse {
pub session_id: SessionId,
#[serde(with = "serde_bytes")]
pub model_contents: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefPatchRequest {
pub session_id: SessionId,
pub ids: HashSet<Ref>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefPatchResponse<'a> {
pub session_id: SessionId,
pub patch: SubscribeMessage<'a>,
}
/// General response type returned from all Rojo routes
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse {
kind: ErrorResponseKind,
details: String,
}
impl ErrorResponse {
pub fn not_found<S: Into<String>>(details: S) -> Self {
Self {
kind: ErrorResponseKind::NotFound,
details: details.into(),
}
}
pub fn bad_request<S: Into<String>>(details: S) -> Self {
Self {
kind: ErrorResponseKind::BadRequest,
details: details.into(),
}
}
pub fn internal_error<S: Into<String>>(details: S) -> Self {
Self {
kind: ErrorResponseKind::InternalError,
details: details.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum ErrorResponseKind {
NotFound,
BadRequest,
InternalError,
}