forked from rojo-rbx/rojo
* Support implicit values for primitive attributes This commit adds support for strings, numbers, and booleans to be implicitly typed in attribute maps, reducing the redundancy of needing to specify their types. I also quietly adjusted one of the tests to use a more stable class/property pair. Since SourceAssetId is locked to Roblox, it could potentially disappear at any time. * Apply formatting. * Address feedback * Backwards compatible format usage. * Axe UnresolvedValueMap in favor of $attributes Attributes can be defined directly on instances, with support for unambiguous types. * Adjust test. * to_string() -> into() * Made attribute test more concise. * small cleanup * Update src/resolution.rs * Update src/resolution.rs * Update src/resolution.rs * Update src/resolution.rs Co-authored-by: Lucien Greathouse <me@lpghatguy.com>
205 lines
5.4 KiB
Rust
205 lines
5.4 KiB
Rust
use std::{borrow::Cow, collections::HashMap, path::Path, str};
|
|
|
|
use anyhow::Context;
|
|
use memofs::Vfs;
|
|
use rbx_dom_weak::types::Attributes;
|
|
use serde::Deserialize;
|
|
|
|
use crate::{
|
|
resolution::UnresolvedValue,
|
|
snapshot::{InstanceContext, InstanceSnapshot},
|
|
};
|
|
|
|
use super::util::PathExt;
|
|
|
|
pub fn snapshot_json_model(
|
|
context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
path: &Path,
|
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
|
let name = path.file_name_trim_end(".model.json")?;
|
|
|
|
let contents = vfs.read(path)?;
|
|
let contents_str = str::from_utf8(&contents)
|
|
.with_context(|| format!("File was not valid UTF-8: {}", path.display()))?;
|
|
|
|
if contents_str.trim().is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mut instance: JsonModel = serde_json::from_str(contents_str)
|
|
.with_context(|| format!("File is not a valid JSON model: {}", path.display()))?;
|
|
|
|
if let Some(top_level_name) = &instance.name {
|
|
let new_name = format!("{}.model.json", top_level_name);
|
|
|
|
log::warn!(
|
|
"Model at path {} had a top-level Name field. \
|
|
This field has been ignored since Rojo 6.0.\n\
|
|
Consider removing this field and renaming the file to {}.",
|
|
new_name,
|
|
path.display()
|
|
);
|
|
}
|
|
|
|
instance.name = Some(name.to_owned());
|
|
|
|
let mut snapshot = instance
|
|
.into_snapshot()
|
|
.with_context(|| format!("Could not load JSON model: {}", path.display()))?;
|
|
|
|
snapshot.metadata = snapshot
|
|
.metadata
|
|
.instigating_source(path)
|
|
.relevant_paths(vec![path.to_path_buf()])
|
|
.context(context);
|
|
|
|
Ok(Some(snapshot))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct JsonModel {
|
|
#[serde(alias = "Name")]
|
|
name: Option<String>,
|
|
|
|
#[serde(alias = "ClassName")]
|
|
class_name: String,
|
|
|
|
#[serde(
|
|
alias = "Children",
|
|
default = "Vec::new",
|
|
skip_serializing_if = "Vec::is_empty"
|
|
)]
|
|
children: Vec<JsonModel>,
|
|
|
|
#[serde(
|
|
alias = "Properties",
|
|
default = "HashMap::new",
|
|
skip_serializing_if = "HashMap::is_empty"
|
|
)]
|
|
properties: HashMap<String, UnresolvedValue>,
|
|
|
|
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
|
|
attributes: HashMap<String, UnresolvedValue>,
|
|
}
|
|
|
|
impl JsonModel {
|
|
fn into_snapshot(self) -> anyhow::Result<InstanceSnapshot> {
|
|
let name = self.name.unwrap_or_else(|| self.class_name.clone());
|
|
let class_name = self.class_name;
|
|
|
|
let mut children = Vec::with_capacity(self.children.len());
|
|
for child in self.children {
|
|
children.push(child.into_snapshot()?);
|
|
}
|
|
|
|
let mut properties = HashMap::with_capacity(self.properties.len());
|
|
for (key, unresolved) in self.properties {
|
|
let value = unresolved.resolve(&class_name, &key)?;
|
|
properties.insert(key, value);
|
|
}
|
|
|
|
if !self.attributes.is_empty() {
|
|
let mut attributes = Attributes::new();
|
|
|
|
for (key, unresolved) in self.attributes {
|
|
let value = unresolved.resolve_unambiguous()?;
|
|
attributes.insert(key, value);
|
|
}
|
|
|
|
properties.insert("Attributes".into(), attributes.into());
|
|
}
|
|
|
|
Ok(InstanceSnapshot {
|
|
snapshot_id: None,
|
|
metadata: Default::default(),
|
|
name: Cow::Owned(name),
|
|
class_name: Cow::Owned(class_name),
|
|
properties,
|
|
children,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
use memofs::{InMemoryFs, VfsSnapshot};
|
|
|
|
#[test]
|
|
fn model_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot(
|
|
"/foo.model.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"className": "IntValue",
|
|
"properties": {
|
|
"Value": 5
|
|
},
|
|
"children": [
|
|
{
|
|
"name": "The Child",
|
|
"className": "StringValue"
|
|
}
|
|
]
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_json_model(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.model.json"),
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
}
|
|
|
|
#[test]
|
|
fn model_from_vfs_legacy() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot(
|
|
"/foo.model.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"ClassName": "IntValue",
|
|
"Properties": {
|
|
"Value": 5
|
|
},
|
|
"Children": [
|
|
{
|
|
"Name": "The Child",
|
|
"ClassName": "StringValue"
|
|
}
|
|
]
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_json_model(
|
|
&InstanceContext::default(),
|
|
&vfs,
|
|
Path::new("/foo.model.json"),
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
}
|
|
}
|