Skip to main content

librojo/snapshot_middleware/
lua.rs

1use std::{path::Path, str};
2
3use anyhow::Context as _;
4use memofs::Vfs;
5use rbx_dom_weak::{
6    types::{Enum, Variant},
7    ustr, HashMapExt as _, UstrMap,
8};
9
10use crate::{
11    snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
12    syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot},
13};
14
15use super::{
16    dir::{snapshot_dir_no_meta, syncback_dir_no_meta},
17    meta_file::{AdjacentMetadata, DirectoryMetadata},
18    PathExt as _,
19};
20
21#[derive(Debug)]
22pub enum ScriptType {
23    Server,
24    Client,
25    Module,
26    Plugin,
27    LegacyServer,
28    LegacyClient,
29    RunContextServer,
30    RunContextClient,
31}
32
33/// Core routine for turning Lua files into snapshots.
34pub fn snapshot_lua(
35    context: &InstanceContext,
36    vfs: &Vfs,
37    path: &Path,
38    name: &str,
39    script_type: ScriptType,
40) -> anyhow::Result<Option<InstanceSnapshot>> {
41    let run_context_enums = &rbx_reflection_database::get()
42        .unwrap()
43        .enums
44        .get("RunContext")
45        .expect("Unable to get RunContext enums!")
46        .items;
47
48    let (class_name, run_context) = match script_type {
49        ScriptType::Server => {
50            if context.emit_legacy_scripts {
51                ("Script", run_context_enums.get("Legacy"))
52            } else {
53                ("Script", run_context_enums.get("Server"))
54            }
55        }
56        ScriptType::Client => {
57            if context.emit_legacy_scripts {
58                ("LocalScript", None)
59            } else {
60                ("Script", run_context_enums.get("Client"))
61            }
62        }
63        ScriptType::Module => ("ModuleScript", None),
64        ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
65        ScriptType::LegacyServer => ("Script", run_context_enums.get("Legacy")),
66        ScriptType::LegacyClient => ("LocalScript", None),
67        ScriptType::RunContextServer => ("Script", run_context_enums.get("Server")),
68        ScriptType::RunContextClient => ("Script", run_context_enums.get("Client")),
69    };
70
71    let contents = vfs.read_to_string_lf_normalized(path)?;
72    let contents_str = contents.as_str();
73
74    let mut properties = UstrMap::with_capacity(2);
75    properties.insert(ustr("Source"), contents_str.into());
76
77    if let Some(run_context) = run_context {
78        properties.insert(
79            ustr("RunContext"),
80            Enum::from_u32(run_context.to_owned()).into(),
81        );
82    }
83
84    let mut snapshot = InstanceSnapshot::new()
85        .name(name)
86        .class_name(class_name)
87        .properties(properties)
88        .metadata(
89            InstanceMetadata::new()
90                .instigating_source(path)
91                .relevant_paths(vec![vfs.canonicalize(path)?])
92                .context(context),
93        );
94
95    AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
96
97    Ok(Some(snapshot))
98}
99
100/// Attempts to snapshot an 'init' Lua script contained inside of a folder with
101/// the given name.
102///
103/// Scripts named `init.lua`, `init.server.lua`, or `init.client.lua` usurp
104/// their parents, which acts similarly to `__init__.py` from the Python world.
105pub fn snapshot_lua_init(
106    context: &InstanceContext,
107    vfs: &Vfs,
108    init_path: &Path,
109    name: &str,
110    script_type: ScriptType,
111) -> anyhow::Result<Option<InstanceSnapshot>> {
112    let folder_path = init_path.parent().unwrap();
113    let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path, name)?.unwrap();
114
115    if dir_snapshot.class_name != "Folder" {
116        anyhow::bail!(
117            "init.lua, init.server.lua, and init.client.lua can \
118             only be used if the instance produced by the containing \
119             directory would be a Folder.\n\
120             \n\
121             The directory {} turned into an instance of class {}.",
122            folder_path.display(),
123            dir_snapshot.class_name
124        );
125    }
126
127    let mut init_snapshot =
128        snapshot_lua(context, vfs, init_path, &dir_snapshot.name, script_type)?.unwrap();
129
130    init_snapshot.children = dir_snapshot.children;
131    init_snapshot.metadata = dir_snapshot.metadata;
132    // The directory snapshot middleware includes all possible init paths
133    // so we don't need to add it here.
134
135    DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?;
136
137    Ok(Some(init_snapshot))
138}
139
140pub fn syncback_lua<'sync>(
141    snapshot: &SyncbackSnapshot<'sync>,
142) -> anyhow::Result<SyncbackReturn<'sync>> {
143    let new_inst = snapshot.new_inst();
144
145    let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
146        source.as_bytes().to_vec()
147    } else {
148        anyhow::bail!("Scripts must have a `Source` property that is a String")
149    };
150    let mut fs_snapshot = FsSnapshot::new();
151    fs_snapshot.add_file(&snapshot.path, contents);
152
153    let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?;
154    if let Some(mut meta) = meta {
155        // Scripts have relatively few properties that we care about, so shifting
156        // is fine.
157        meta.properties.shift_remove(&ustr("Source"));
158
159        if !meta.is_empty() {
160            let parent_location = snapshot.path.parent_err()?;
161            fs_snapshot.add_file(
162                parent_location.join(format!("{}.meta.json", new_inst.name)),
163                serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
164            );
165        }
166    }
167
168    Ok(SyncbackReturn {
169        fs_snapshot,
170        // Scripts don't have a child!
171        children: Vec::new(),
172        removed_children: Vec::new(),
173    })
174}
175
176pub fn syncback_lua_init<'sync>(
177    script_type: ScriptType,
178    snapshot: &SyncbackSnapshot<'sync>,
179) -> anyhow::Result<SyncbackReturn<'sync>> {
180    let new_inst = snapshot.new_inst();
181    let path = snapshot.path.join(match script_type {
182        ScriptType::Server => "init.server.luau",
183        ScriptType::Client => "init.client.luau",
184        ScriptType::Module => "init.luau",
185        ScriptType::Plugin => "init.plugin.luau",
186        _ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"),
187    });
188
189    let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
190        source.as_bytes().to_vec()
191    } else {
192        anyhow::bail!("Scripts must have a `Source` property that is a String")
193    };
194
195    let mut dir_syncback = syncback_dir_no_meta(snapshot)?;
196    dir_syncback.fs_snapshot.add_file(&path, contents);
197
198    let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, path.clone())?;
199    if let Some(mut meta) = meta {
200        // Scripts have relatively few properties that we care about, so shifting
201        // is fine.
202        meta.properties.shift_remove(&ustr("Source"));
203
204        if !meta.is_empty() {
205            dir_syncback.fs_snapshot.add_file(
206                snapshot.path.join("init.meta.json"),
207                serde_json::to_vec_pretty(&meta)
208                    .context("could not serialize new init.meta.json")?,
209            );
210        }
211    }
212
213    Ok(dir_syncback)
214}
215
216#[cfg(test)]
217mod test {
218    use super::*;
219
220    use memofs::{InMemoryFs, VfsSnapshot};
221
222    #[test]
223    fn class_module_from_vfs() {
224        let mut imfs = InMemoryFs::new();
225        imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
226            .unwrap();
227
228        let vfs = Vfs::new(imfs);
229
230        let instance_snapshot = snapshot_lua(
231            &InstanceContext::with_emit_legacy_scripts(Some(true)),
232            &vfs,
233            Path::new("/foo.lua"),
234            "foo",
235            ScriptType::Module,
236        )
237        .unwrap()
238        .unwrap();
239
240        insta::with_settings!({ sort_maps => true }, {
241            insta::assert_yaml_snapshot!(instance_snapshot);
242        });
243    }
244
245    #[test]
246    fn runcontext_module_from_vfs() {
247        let mut imfs = InMemoryFs::new();
248        imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
249            .unwrap();
250
251        let vfs = Vfs::new(imfs);
252
253        let instance_snapshot = snapshot_lua(
254            &InstanceContext::with_emit_legacy_scripts(Some(false)),
255            &vfs,
256            Path::new("/foo.lua"),
257            "foo",
258            ScriptType::Module,
259        )
260        .unwrap()
261        .unwrap();
262
263        insta::with_settings!({ sort_maps => true }, {
264            insta::assert_yaml_snapshot!(instance_snapshot);
265        });
266    }
267
268    #[test]
269    fn plugin_module_from_vfs() {
270        let mut imfs = InMemoryFs::new();
271        imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
272            .unwrap();
273
274        let vfs = Vfs::new(imfs);
275
276        let instance_snapshot = snapshot_lua(
277            &InstanceContext::with_emit_legacy_scripts(Some(false)),
278            &vfs,
279            Path::new("/foo.plugin.lua"),
280            "foo",
281            ScriptType::Plugin,
282        )
283        .unwrap()
284        .unwrap();
285
286        insta::with_settings!({ sort_maps => true }, {
287            insta::assert_yaml_snapshot!(instance_snapshot);
288        });
289    }
290
291    #[test]
292    fn class_server_from_vfs() {
293        let mut imfs = InMemoryFs::new();
294        imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
295            .unwrap();
296
297        let vfs = Vfs::new(imfs);
298
299        let instance_snapshot = snapshot_lua(
300            &InstanceContext::with_emit_legacy_scripts(Some(true)),
301            &vfs,
302            Path::new("/foo.server.lua"),
303            "foo",
304            ScriptType::Server,
305        )
306        .unwrap()
307        .unwrap();
308
309        insta::with_settings!({ sort_maps => true }, {
310            insta::assert_yaml_snapshot!(instance_snapshot);
311        });
312    }
313
314    #[test]
315    fn runcontext_server_from_vfs() {
316        let mut imfs = InMemoryFs::new();
317        imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
318            .unwrap();
319
320        let vfs = Vfs::new(imfs);
321
322        let instance_snapshot = snapshot_lua(
323            &InstanceContext::with_emit_legacy_scripts(Some(false)),
324            &vfs,
325            Path::new("/foo.server.lua"),
326            "foo",
327            ScriptType::Server,
328        )
329        .unwrap()
330        .unwrap();
331
332        insta::with_settings!({ sort_maps => true }, {
333            insta::assert_yaml_snapshot!(instance_snapshot);
334        });
335    }
336
337    #[test]
338    fn class_client_from_vfs() {
339        let mut imfs = InMemoryFs::new();
340        imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
341            .unwrap();
342
343        let vfs = Vfs::new(imfs);
344
345        let instance_snapshot = snapshot_lua(
346            &InstanceContext::with_emit_legacy_scripts(Some(true)),
347            &vfs,
348            Path::new("/foo.client.lua"),
349            "foo",
350            ScriptType::Client,
351        )
352        .unwrap()
353        .unwrap();
354
355        insta::with_settings!({ sort_maps => true }, {
356            insta::assert_yaml_snapshot!(instance_snapshot);
357        });
358    }
359
360    #[test]
361    fn runcontext_client_from_vfs() {
362        let mut imfs = InMemoryFs::new();
363        imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
364            .unwrap();
365
366        let vfs = Vfs::new(imfs);
367
368        let instance_snapshot = snapshot_lua(
369            &InstanceContext::with_emit_legacy_scripts(Some(false)),
370            &vfs,
371            Path::new("/foo.client.lua"),
372            "foo",
373            ScriptType::Client,
374        )
375        .unwrap()
376        .unwrap();
377
378        insta::with_settings!({ sort_maps => true }, {
379            insta::assert_yaml_snapshot!(instance_snapshot);
380        });
381    }
382
383    #[test]
384    fn init_module_from_vfs() {
385        let mut imfs = InMemoryFs::new();
386        imfs.load_snapshot(
387            "/root",
388            VfsSnapshot::dir([("init.lua", VfsSnapshot::file("Hello!"))]),
389        )
390        .unwrap();
391
392        let vfs = Vfs::new(imfs);
393
394        let instance_snapshot = snapshot_lua_init(
395            &InstanceContext::with_emit_legacy_scripts(Some(true)),
396            &vfs,
397            Path::new("/root/init.lua"),
398            "root",
399            ScriptType::Module,
400        )
401        .unwrap()
402        .unwrap();
403
404        insta::with_settings!({ sort_maps => true }, {
405            insta::assert_yaml_snapshot!(instance_snapshot);
406        });
407    }
408
409    #[test]
410    fn init_module_from_vfs_with_meta() {
411        let mut imfs = InMemoryFs::new();
412        imfs.load_snapshot(
413            "/root",
414            VfsSnapshot::dir([
415                ("init.lua", VfsSnapshot::file("Hello!")),
416                (
417                    "init.meta.json",
418                    VfsSnapshot::file(r#"{"id": "manually specified"}"#),
419                ),
420            ]),
421        )
422        .unwrap();
423
424        let vfs = Vfs::new(imfs);
425
426        let instance_snapshot = snapshot_lua_init(
427            &InstanceContext::with_emit_legacy_scripts(Some(true)),
428            &vfs,
429            Path::new("/root/init.lua"),
430            "root",
431            ScriptType::Module,
432        )
433        .unwrap()
434        .unwrap();
435
436        insta::with_settings!({ sort_maps => true }, {
437            insta::assert_yaml_snapshot!(instance_snapshot);
438        });
439    }
440
441    #[test]
442    fn class_module_with_meta() {
443        let mut imfs = InMemoryFs::new();
444        imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
445            .unwrap();
446        imfs.load_snapshot(
447            "/foo.meta.json",
448            VfsSnapshot::file(
449                r#"
450                    {
451                        "ignoreUnknownInstances": true
452                    }
453                "#,
454            ),
455        )
456        .unwrap();
457
458        let vfs = Vfs::new(imfs);
459
460        let instance_snapshot = snapshot_lua(
461            &InstanceContext::with_emit_legacy_scripts(Some(true)),
462            &vfs,
463            Path::new("/foo.lua"),
464            "foo",
465            ScriptType::Module,
466        )
467        .unwrap()
468        .unwrap();
469
470        insta::with_settings!({ sort_maps => true }, {
471            insta::assert_yaml_snapshot!(instance_snapshot);
472        });
473    }
474
475    #[test]
476    fn runcontext_module_with_meta() {
477        let mut imfs = InMemoryFs::new();
478        imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
479            .unwrap();
480        imfs.load_snapshot(
481            "/foo.meta.json",
482            VfsSnapshot::file(
483                r#"
484                    {
485                        "ignoreUnknownInstances": true
486                    }
487                "#,
488            ),
489        )
490        .unwrap();
491
492        let vfs = Vfs::new(imfs);
493
494        let instance_snapshot = snapshot_lua(
495            &InstanceContext::with_emit_legacy_scripts(Some(false)),
496            &vfs,
497            Path::new("/foo.lua"),
498            "foo",
499            ScriptType::Module,
500        )
501        .unwrap()
502        .unwrap();
503
504        insta::with_settings!({ sort_maps => true }, {
505            insta::assert_yaml_snapshot!(instance_snapshot);
506        });
507    }
508
509    #[test]
510    fn class_script_with_meta() {
511        let mut imfs = InMemoryFs::new();
512        imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
513            .unwrap();
514        imfs.load_snapshot(
515            "/foo.meta.json",
516            VfsSnapshot::file(
517                r#"
518                    {
519                        "ignoreUnknownInstances": true
520                    }
521                "#,
522            ),
523        )
524        .unwrap();
525
526        let vfs = Vfs::new(imfs);
527
528        let instance_snapshot = snapshot_lua(
529            &InstanceContext::with_emit_legacy_scripts(Some(true)),
530            &vfs,
531            Path::new("/foo.server.lua"),
532            "foo",
533            ScriptType::Server,
534        )
535        .unwrap()
536        .unwrap();
537
538        insta::with_settings!({ sort_maps => true }, {
539            insta::assert_yaml_snapshot!(instance_snapshot);
540        });
541    }
542
543    #[test]
544    fn runcontext_script_with_meta() {
545        let mut imfs = InMemoryFs::new();
546        imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
547            .unwrap();
548        imfs.load_snapshot(
549            "/foo.meta.json",
550            VfsSnapshot::file(
551                r#"
552                    {
553                        "ignoreUnknownInstances": true
554                    }
555                "#,
556            ),
557        )
558        .unwrap();
559
560        let vfs = Vfs::new(imfs);
561
562        let instance_snapshot = snapshot_lua(
563            &InstanceContext::with_emit_legacy_scripts(Some(false)),
564            &vfs,
565            Path::new("/foo.server.lua"),
566            "foo",
567            ScriptType::Server,
568        )
569        .unwrap()
570        .unwrap();
571
572        insta::with_settings!({ sort_maps => true }, {
573            insta::assert_yaml_snapshot!(instance_snapshot);
574        });
575    }
576
577    #[test]
578    fn class_script_disabled() {
579        let mut imfs = InMemoryFs::new();
580        imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
581            .unwrap();
582        imfs.load_snapshot(
583            "/bar.meta.json",
584            VfsSnapshot::file(
585                r#"
586                    {
587                        "properties": {
588                            "Disabled": true
589                        }
590                    }
591                "#,
592            ),
593        )
594        .unwrap();
595
596        let vfs = Vfs::new(imfs);
597
598        let instance_snapshot = snapshot_lua(
599            &InstanceContext::with_emit_legacy_scripts(Some(true)),
600            &vfs,
601            Path::new("/bar.server.lua"),
602            "bar",
603            ScriptType::Server,
604        )
605        .unwrap()
606        .unwrap();
607
608        insta::with_settings!({ sort_maps => true }, {
609            insta::assert_yaml_snapshot!(instance_snapshot);
610        });
611    }
612
613    #[test]
614    fn runcontext_script_disabled() {
615        let mut imfs = InMemoryFs::new();
616        imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
617            .unwrap();
618        imfs.load_snapshot(
619            "/bar.meta.json",
620            VfsSnapshot::file(
621                r#"
622                    {
623                        "properties": {
624                            "Disabled": true
625                        }
626                    }
627                "#,
628            ),
629        )
630        .unwrap();
631
632        let vfs = Vfs::new(imfs);
633
634        let instance_snapshot = snapshot_lua(
635            &InstanceContext::with_emit_legacy_scripts(Some(false)),
636            &vfs,
637            Path::new("/bar.server.lua"),
638            "bar",
639            ScriptType::Server,
640        )
641        .unwrap()
642        .unwrap();
643
644        insta::with_settings!({ sort_maps => true }, {
645            insta::assert_yaml_snapshot!(instance_snapshot);
646        });
647    }
648}