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