Skip to main content

librojo/web/
interface.rs

1//! Defines all the structs needed to interact with the Rojo Serve API. This is
2//! useful for tests to be able to use the same data structures as the
3//! implementation.
4
5use std::{
6    borrow::Cow,
7    collections::{HashMap, HashSet},
8};
9
10use rbx_dom_weak::{
11    types::{Ref, Variant, VariantType},
12    Ustr, UstrMap,
13};
14use serde::{Deserialize, Serialize};
15use strum::Display;
16
17use crate::{
18    session_id::SessionId,
19    snapshot::{
20        AppliedPatchSet, InstanceMetadata as RojoInstanceMetadata, InstanceWithMeta, RojoTree,
21    },
22};
23
24/// Server version to report over the API, not exposed outside this crate.
25pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
26
27/// Current protocol version, which is required to match.
28pub const PROTOCOL_VERSION: u64 = 5;
29
30/// Message returned by Rojo API when a change has occurred.
31#[derive(Debug, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct SubscribeMessage<'a> {
34    pub removed: Vec<Ref>,
35    pub added: HashMap<Ref, Instance<'a>>,
36    pub updated: Vec<InstanceUpdate>,
37}
38
39impl<'a> SubscribeMessage<'a> {
40    pub(crate) fn from_patch_update(tree: &'a RojoTree, patch: AppliedPatchSet) -> Self {
41        let removed = patch.removed;
42
43        let mut added = HashMap::new();
44        for id in patch.added {
45            let instance = tree.get_instance(id).unwrap();
46            added.insert(id, Instance::from_rojo_instance(instance));
47
48            for instance in tree.descendants(id) {
49                added.insert(instance.id(), Instance::from_rojo_instance(instance));
50            }
51        }
52
53        let updated = patch
54            .updated
55            .into_iter()
56            .map(|update| {
57                let changed_metadata = update
58                    .changed_metadata
59                    .as_ref()
60                    .map(InstanceMetadata::from_rojo_metadata);
61
62                let changed_properties = update
63                    .changed_properties
64                    .into_iter()
65                    .filter(|(_key, value)| property_filter(value.as_ref()))
66                    .collect();
67
68                InstanceUpdate {
69                    id: update.id,
70                    changed_name: update.changed_name,
71                    changed_class_name: update.changed_class_name,
72                    changed_properties,
73                    changed_metadata,
74                }
75            })
76            .collect();
77
78        Self {
79            removed,
80            added,
81            updated,
82        }
83    }
84}
85
86#[derive(Debug, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct InstanceUpdate {
89    pub id: Ref,
90    pub changed_name: Option<String>,
91    pub changed_class_name: Option<Ustr>,
92
93    // TODO: Transform from UstrMap<String, Option<_>> to something else, since
94    // null will get lost when decoding from JSON in some languages.
95    #[serde(default)]
96    pub changed_properties: UstrMap<Option<Variant>>,
97    pub changed_metadata: Option<InstanceMetadata>,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct InstanceMetadata {
103    pub ignore_unknown_instances: bool,
104}
105
106impl InstanceMetadata {
107    pub(crate) fn from_rojo_metadata(meta: &RojoInstanceMetadata) -> Self {
108        Self {
109            ignore_unknown_instances: meta.ignore_unknown_instances,
110        }
111    }
112}
113
114#[derive(Debug, Serialize, Deserialize)]
115#[serde(rename_all = "PascalCase")]
116pub struct Instance<'a> {
117    pub id: Ref,
118    pub parent: Ref,
119    pub name: Cow<'a, str>,
120    pub class_name: Ustr,
121    pub properties: UstrMap<Cow<'a, Variant>>,
122    pub children: Cow<'a, [Ref]>,
123    pub metadata: Option<InstanceMetadata>,
124}
125
126impl Instance<'_> {
127    pub(crate) fn from_rojo_instance(source: InstanceWithMeta<'_>) -> Instance<'_> {
128        let properties = source
129            .properties()
130            .iter()
131            .filter(|(_key, value)| property_filter(Some(value)))
132            .map(|(key, value)| (*key, Cow::Borrowed(value)))
133            .collect();
134
135        Instance {
136            id: source.id(),
137            parent: source.parent(),
138            name: Cow::Borrowed(source.name()),
139            class_name: source.class_name(),
140            properties,
141            children: Cow::Borrowed(source.children()),
142            metadata: Some(InstanceMetadata::from_rojo_metadata(source.metadata())),
143        }
144    }
145}
146
147fn property_filter(value: Option<&Variant>) -> bool {
148    let ty = value.map(|value| value.ty());
149
150    // Lua can't do anything with SharedString values. They also can't be
151    // serialized directly by Serde!
152    ty != Some(VariantType::SharedString)
153}
154
155/// Response body from /api/rojo
156#[derive(Debug, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct ServerInfoResponse {
159    pub session_id: SessionId,
160    pub server_version: String,
161    pub protocol_version: u64,
162    pub project_name: String,
163    pub expected_place_ids: Option<HashSet<u64>>,
164    pub unexpected_place_ids: Option<HashSet<u64>>,
165    pub game_id: Option<u64>,
166    pub place_id: Option<u64>,
167    pub root_instance_id: Ref,
168}
169
170/// Response body from /api/read/{id}
171#[derive(Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ReadResponse<'a> {
174    pub session_id: SessionId,
175    pub message_cursor: u32,
176    pub instances: HashMap<Ref, Instance<'a>>,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct WriteRequest {
182    pub session_id: SessionId,
183    pub removed: Vec<Ref>,
184
185    #[serde(default)]
186    pub added: HashMap<Ref, ()>,
187    pub updated: Vec<InstanceUpdate>,
188}
189
190#[derive(Debug, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192pub struct WriteResponse {
193    pub session_id: SessionId,
194}
195
196/// Packet type enum for different websocket message types
197#[derive(Debug, Serialize, Deserialize, Display, Eq, PartialEq)]
198#[serde(rename_all = "camelCase")]
199#[strum(serialize_all = "camelCase")]
200pub enum SocketPacketType {
201    Messages,
202    // TODO: Can we cleanly use the socket for all communication?
203    // Serialize,
204    // RefPatch,
205}
206
207/// Body content for messages packet type
208#[derive(Debug, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct MessagesPacket<'a> {
211    pub message_cursor: u32,
212    pub messages: Vec<SubscribeMessage<'a>>,
213}
214
215/// Body content for different packet types
216#[derive(Debug, Serialize, Deserialize)]
217#[serde(untagged)]
218pub enum SocketPacketBody<'a> {
219    Messages(MessagesPacket<'a>),
220    // TODO: Can we cleanly use the socket for all communication?
221    // Serialize(SerializePacket),
222    // RefPatch(RefPatchPacket<'a>),
223}
224
225/// Message content from /api/socket
226#[derive(Debug, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct SocketPacket<'a> {
229    pub session_id: SessionId,
230    pub packet_type: SocketPacketType,
231    pub body: SocketPacketBody<'a>,
232}
233
234/// Response body from /api/open/{id}
235#[derive(Debug, Serialize, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct OpenResponse {
238    pub session_id: SessionId,
239}
240
241#[derive(Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct SerializeRequest {
244    pub session_id: SessionId,
245    pub ids: Vec<Ref>,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct SerializeResponse {
251    pub session_id: SessionId,
252    #[serde(with = "serde_bytes")]
253    pub model_contents: Vec<u8>,
254}
255
256#[derive(Debug, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct RefPatchRequest {
259    pub session_id: SessionId,
260    pub ids: HashSet<Ref>,
261}
262
263#[derive(Debug, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct RefPatchResponse<'a> {
266    pub session_id: SessionId,
267    pub patch: SubscribeMessage<'a>,
268}
269
270/// General response type returned from all Rojo routes
271#[derive(Debug, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase")]
273pub struct ErrorResponse {
274    kind: ErrorResponseKind,
275    details: String,
276}
277
278impl ErrorResponse {
279    pub fn not_found<S: Into<String>>(details: S) -> Self {
280        Self {
281            kind: ErrorResponseKind::NotFound,
282            details: details.into(),
283        }
284    }
285
286    pub fn bad_request<S: Into<String>>(details: S) -> Self {
287        Self {
288            kind: ErrorResponseKind::BadRequest,
289            details: details.into(),
290        }
291    }
292
293    pub fn forbidden<S: Into<String>>(details: S) -> Self {
294        Self {
295            kind: ErrorResponseKind::Forbidden,
296            details: details.into(),
297        }
298    }
299
300    pub fn internal_error<S: Into<String>>(details: S) -> Self {
301        Self {
302            kind: ErrorResponseKind::InternalError,
303            details: details.into(),
304        }
305    }
306}
307
308#[derive(Debug, Serialize, Deserialize)]
309pub enum ErrorResponseKind {
310    NotFound,
311    BadRequest,
312    Forbidden,
313    InternalError,
314}