use std::{borrow::Cow, collections::HashMap, path::Path, str}; use anyhow::Context; use memofs::Vfs; 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> { 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, #[serde(alias = "ClassName")] class_name: String, #[serde( alias = "Children", default = "Vec::new", skip_serializing_if = "Vec::is_empty" )] children: Vec, #[serde( alias = "Properties", default = "HashMap::new", skip_serializing_if = "HashMap::is_empty" )] properties: HashMap, } impl JsonModel { fn into_snapshot(self) -> anyhow::Result { 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); } 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); } }