Add YAML middleware that behaves like TOML and JSON (#1093)

This commit is contained in:
Micah
2025-08-02 20:58:13 -07:00
committed by GitHub
parent 3002d250a1
commit a4eb65ca3f
7 changed files with 332 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ mod rbxmx;
mod toml;
mod txt;
mod util;
mod yaml;
use std::{
path::{Path, PathBuf},
@@ -41,6 +42,7 @@ use self::{
rbxmx::snapshot_rbxmx,
toml::snapshot_toml,
txt::snapshot_txt,
yaml::snapshot_yaml,
};
pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default};
@@ -212,6 +214,7 @@ pub enum Middleware {
Rbxmx,
Toml,
Text,
Yaml,
Ignore,
}
@@ -250,6 +253,7 @@ impl Middleware {
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
Self::Toml => snapshot_toml(context, vfs, path, name),
Self::Text => snapshot_txt(context, vfs, path, name),
Self::Yaml => snapshot_yaml(context, vfs, path, name),
Self::Ignore => Ok(None),
}
}
@@ -315,6 +319,7 @@ pub fn default_sync_rules() -> &'static [SyncRule] {
sync_rule!("*.txt", Text),
sync_rule!("*.rbxmx", Rbxmx),
sync_rule!("*.rbxm", Rbxm),
sync_rule!("*.{yml,yaml}", Yaml),
]
})
}

View File

@@ -0,0 +1,27 @@
---
source: src/snapshot_middleware/yaml.rs
expression: source
---
return {
string = "this is a string",
boolean = true,
integer = 1337,
float = 123456789.5,
["value-with-hypen"] = "it sure is",
sequence = {"wow", 8675309},
map = {
key = "value",
key2 = "value 2",
key3 = "value 3",
},
["nested-map"] = {{
key = "value",
}, {
key2 = "value 2",
}, {
key3 = "value 3",
}},
whatever_this_is = {"i imagine", "it's", "a", "sequence?"},
null1 = nil,
null2 = nil,
}

View File

@@ -0,0 +1,21 @@
---
source: src/snapshot_middleware/yaml.rs
expression: instance_snapshot
---
snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
Path: /foo.yaml
relevant_paths:
- /foo.yaml
- /foo.meta.json
context:
emit_legacy_scripts: true
specified_id: ~
name: foo
class_name: ModuleScript
properties:
Source:
String: "return {\n\tstring = \"this is a string\",\n\tboolean = true,\n\tinteger = 1337,\n\tfloat = 123456789.5,\n\t[\"value-with-hypen\"] = \"it sure is\",\n\tsequence = {\"wow\", 8675309},\n\tmap = {\n\t\tkey = \"value\",\n\t\tkey2 = \"value 2\",\n\t\tkey3 = \"value 3\",\n\t},\n\t[\"nested-map\"] = {{\n\t\tkey = \"value\",\n\t}, {\n\t\tkey2 = \"value 2\",\n\t}, {\n\t\tkey3 = \"value 3\",\n\t}},\n\twhatever_this_is = {\"i imagine\", \"it's\", \"a\", \"sequence?\"},\n\tnull1 = nil,\n\tnull2 = nil,\n}"
children: []

View File

@@ -0,0 +1,234 @@
use std::path::Path;
use anyhow::Context as _;
use memofs::{IoResultExt, Vfs};
use rbx_dom_weak::ustr;
use yaml_rust2::{Yaml, YamlLoader};
use crate::{
lua_ast::{Expression, Statement},
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
};
use super::meta_file::AdjacentMetadata;
pub fn snapshot_yaml(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
name: &str,
) -> anyhow::Result<Option<InstanceSnapshot>> {
let contents = vfs.read_to_string(path)?;
let mut values = YamlLoader::load_from_str(&contents)?;
let value = values
.pop()
.context("all YAML documents must contain a document")?;
if !values.is_empty() {
anyhow::bail!("Rojo does not currently support multiple documents in a YAML file")
}
let as_lua = Statement::Return(yaml_to_luau(value)?);
let meta_path = path.with_file_name(format!("{}.meta.json", name));
let mut snapshot = InstanceSnapshot::new()
.name(name)
.class_name("ModuleScript")
.property(ustr("Source"), as_lua.to_string())
.metadata(
InstanceMetadata::new()
.instigating_source(path)
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()])
.context(context),
);
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
metadata.apply_all(&mut snapshot)?;
}
Ok(Some(snapshot))
}
fn yaml_to_luau(value: Yaml) -> anyhow::Result<Expression> {
const MAX_FLOAT_INT: i64 = 1 << 53;
Ok(match value {
Yaml::String(str) => Expression::String(str),
Yaml::Boolean(bool) => Expression::Bool(bool),
Yaml::Integer(int) => {
if int <= MAX_FLOAT_INT {
Expression::Number(int as f64)
} else {
anyhow::bail!(
"the integer '{int}' cannot be losslessly converted into a Luau number"
)
}
}
Yaml::Real(_) => {
let value = value.as_f64().expect("value should be a valid f64");
Expression::Number(value)
}
Yaml::Null => Expression::Nil,
Yaml::Array(values) => {
let new_values: anyhow::Result<Vec<Expression>> =
values.into_iter().map(yaml_to_luau).collect();
Expression::Array(new_values?)
}
Yaml::Hash(map) => {
let new_values: anyhow::Result<Vec<(Expression, Expression)>> = map
.into_iter()
.map(|(k, v)| {
let k = yaml_to_luau(k)?;
let v = yaml_to_luau(v)?;
Ok((k, v))
})
.collect();
Expression::table(new_values?)
}
Yaml::Alias(_) => {
anyhow::bail!("Rojo cannot convert YAML aliases to Luau")
}
Yaml::BadValue => {
anyhow::bail!("Rojo cannot convert YAML to Luau because of a parsing error")
}
})
}
#[cfg(test)]
mod test {
use super::*;
use memofs::{InMemoryFs, VfsSnapshot};
use rbx_dom_weak::types::Variant;
#[test]
fn instance_from_vfs() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.yaml",
VfsSnapshot::file(
r#"
---
string: this is a string
boolean: true
integer: 1337
float: 123456789.5
value-with-hypen: it sure is
sequence:
- wow
- 8675309
map:
key: value
key2: "value 2"
key3: 'value 3'
nested-map:
- key: value
- key2: "value 2"
- key3: 'value 3'
whatever_this_is: [i imagine, it's, a, sequence?]
null1: ~
null2: null"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs.clone());
let instance_snapshot = snapshot_yaml(
&InstanceContext::default(),
&vfs,
Path::new("/foo.yaml"),
"foo",
)
.unwrap()
.unwrap();
insta::assert_yaml_snapshot!(instance_snapshot);
let source = instance_snapshot
.properties
.get(&ustr("Source"))
.expect("the result from snapshot_yaml should have a Source property");
if let Variant::String(source) = source {
insta::assert_snapshot!(source)
} else {
panic!("the Source property from snapshot_yaml was not a String")
}
}
#[test]
#[should_panic(expected = "multiple documents")]
fn multiple_documents() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/foo.yaml",
VfsSnapshot::file(
r#"
---
document-1: this is a document
---
document-2: this is also a document"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs.clone());
snapshot_yaml(
&InstanceContext::default(),
&vfs,
Path::new("/foo.yaml"),
"foo",
)
.unwrap()
.unwrap();
}
#[test]
#[should_panic = "cannot be losslessly converted into a Luau number"]
fn integer_border() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot(
"/allowed.yaml",
VfsSnapshot::file(
r#"
value: 9007199254740992
"#,
),
)
.unwrap();
imfs.load_snapshot(
"/not-allowed.yaml",
VfsSnapshot::file(
r#"
value: 9007199254740993
"#,
),
)
.unwrap();
let vfs = Vfs::new(imfs.clone());
assert!(
snapshot_yaml(
&InstanceContext::default(),
&vfs,
Path::new("/allowed.yaml"),
"allowed",
)
.is_ok(),
"snapshot_yaml failed to snapshot document with integer '9007199254740992' in it"
);
snapshot_yaml(
&InstanceContext::default(),
&vfs,
Path::new("/not-allowed.yaml"),
"not-allowed",
)
.unwrap()
.unwrap();
}
}