rojo/src/syncback/meta.rs
Joe 6f8e5a5cf2 Refactor code for improved readability and consistency
- Simplified function calls and removed unnecessary line breaks in `change_processor.rs`, `rojo_ref.rs`, `syncback/meta.rs`, `syncback/mod.rs`, `syncback/ref_properties.rs`, and `web/api.rs` to enhance code clarity.
- Updated test assertions in `two_way_sync.rs` for better readability by consolidating multiple lines into single assertions.

These changes improve the overall maintainability and readability of the codebase.
2026-02-18 04:21:50 -06:00

185 lines
6.9 KiB
Rust

//! Shared helpers for reading and writing the `name` field in `.meta.json5` files.
//!
//! These are used by both `change_processor.rs` (two-way sync renames) and
//! `web/api.rs` (syncback added/removed instances) to keep meta file handling
//! DRY and consistent.
use anyhow::Context;
use std::fs;
use std::path::Path;
/// Upsert the `name` field in a `.meta.json5` file.
///
/// If the file exists, parses it and merges the `name` key (preserving other
/// fields like `ignoreUnknownInstances`). If it doesn't exist, creates a new
/// file with just the `name` key.
///
/// Returns `Ok(true)` if the file was written, `Ok(false)` if serialization
/// failed (logged), or `Err` on I/O failure.
pub fn upsert_meta_name(meta_path: &Path, real_name: &str) -> anyhow::Result<()> {
let mut obj = if meta_path.exists() {
match fs::read(meta_path) {
Ok(bytes) => match crate::json::from_slice::<serde_json::Value>(&bytes) {
Ok(serde_json::Value::Object(map)) => map,
_ => serde_json::Map::new(),
},
Err(_) => serde_json::Map::new(),
}
} else {
serde_json::Map::new()
};
obj.insert(
"name".to_string(),
serde_json::Value::String(real_name.to_string()),
);
let content = crate::json::to_vec_pretty_sorted(&serde_json::Value::Object(obj))?;
fs::write(meta_path, &content)?;
Ok(())
}
/// Outcome of attempting to remove the `name` field from a meta file.
pub enum RemoveNameOutcome {
/// The meta file didn't exist or had no `name` field -- nothing changed.
NoOp,
/// The `name` field was removed; other fields remain. File was rewritten.
FieldRemoved,
/// The `name` field was the only field. The file was deleted entirely.
FileDeleted,
}
/// Upsert the `name` field inside a `.model.json5` / `.model.json` file.
///
/// Parses the existing JSON, sets/replaces the `name` key, and rewrites.
/// Unlike `upsert_meta_name`, this modifies the model file in-place.
pub fn upsert_model_name(model_path: &Path, real_name: &str) -> anyhow::Result<()> {
let bytes = fs::read(model_path)?;
let mut obj = match crate::json::from_slice::<serde_json::Value>(&bytes) {
Ok(serde_json::Value::Object(map)) => map,
_ => anyhow::bail!("model file is not a JSON object: {}", model_path.display()),
};
obj.insert(
"name".to_string(),
serde_json::Value::String(real_name.to_string()),
);
let content = crate::json::to_vec_pretty_sorted(&serde_json::Value::Object(obj))?;
fs::write(model_path, &content)?;
Ok(())
}
/// Remove the `name` field from a `.model.json5` / `.model.json` file.
///
/// Unlike meta files, model files are never deleted when they become "empty"
/// (they always have at least `className`). Returns `RemoveNameOutcome` for
/// consistency with the meta helpers.
pub fn remove_model_name(model_path: &Path) -> anyhow::Result<RemoveNameOutcome> {
if !model_path.exists() {
return Ok(RemoveNameOutcome::NoOp);
}
let bytes = match fs::read(model_path) {
Ok(b) => b,
Err(_) => return Ok(RemoveNameOutcome::NoOp),
};
let mut obj = match crate::json::from_slice::<serde_json::Value>(&bytes) {
Ok(serde_json::Value::Object(map)) => map,
_ => return Ok(RemoveNameOutcome::NoOp),
};
if obj.remove("name").is_none() {
return Ok(RemoveNameOutcome::NoOp);
}
let content = crate::json::to_vec_pretty_sorted(&serde_json::Value::Object(obj))?;
fs::write(model_path, &content)?;
Ok(RemoveNameOutcome::FieldRemoved)
}
/// Update `Rojo_Ref_*` attribute paths in a meta/model JSON5 file.
///
/// For each attribute whose key starts with `Rojo_Ref_` and whose string
/// value starts with `old_prefix`, replaces the prefix with `new_prefix`.
/// Returns true if any attribute was updated.
/// Update `Rojo_Ref_*` attributes in a meta/model file after a rename.
///
/// For each attribute, resolves the on-disk relative path to absolute using
/// `source_abs`, checks if the resolved absolute path is affected by the
/// rename (`old_prefix` → `new_prefix`), and if so, recomputes the relative
/// path from `source_abs` to the new absolute target.
pub fn update_ref_paths_in_file(
file_path: &Path,
old_prefix: &str,
new_prefix: &str,
source_abs: &str,
) -> anyhow::Result<bool> {
use crate::REF_PATH_ATTRIBUTE_PREFIX;
if !file_path.exists() {
return Ok(false);
}
let bytes =
fs::read(file_path).with_context(|| format!("Failed to read {}", file_path.display()))?;
let mut val: serde_json::Value = crate::json::from_slice(&bytes)
.with_context(|| format!("Failed to parse JSON5 in {}", file_path.display()))?;
if !val.is_object() {
anyhow::bail!(
"{} is not a JSON object, cannot update Rojo_Ref_* attributes",
file_path.display()
);
}
let old_prefix_slash = format!("{old_prefix}/");
let mut updated = false;
if let Some(attrs) = val.get_mut("attributes").and_then(|a| a.as_object_mut()) {
for (key, value) in attrs.iter_mut() {
if !key.starts_with(REF_PATH_ATTRIBUTE_PREFIX) {
continue;
}
let Some(path_str) = value.as_str() else {
continue;
};
let Some(resolved) = crate::resolve_ref_path_to_absolute(path_str, source_abs) else {
continue;
};
if resolved == old_prefix || resolved.starts_with(&old_prefix_slash) {
let new_abs = format!("{new_prefix}{}", &resolved[old_prefix.len()..]);
let new_relative = crate::compute_relative_ref_path(source_abs, &new_abs);
*value = serde_json::Value::String(new_relative);
updated = true;
}
}
}
if updated {
let content = crate::json::to_vec_pretty_sorted(&val)?;
fs::write(file_path, &content)?;
}
Ok(updated)
}
/// Remove the `name` field from a `.meta.json5` file.
///
/// If the file becomes an empty object after removal, deletes it entirely.
/// Returns the outcome so callers can manage filesystem event suppression.
pub fn remove_meta_name(meta_path: &Path) -> anyhow::Result<RemoveNameOutcome> {
if !meta_path.exists() {
return Ok(RemoveNameOutcome::NoOp);
}
let bytes = match fs::read(meta_path) {
Ok(b) => b,
Err(_) => return Ok(RemoveNameOutcome::NoOp),
};
let mut obj = match crate::json::from_slice::<serde_json::Value>(&bytes) {
Ok(serde_json::Value::Object(map)) => map,
_ => return Ok(RemoveNameOutcome::NoOp),
};
if obj.remove("name").is_none() {
return Ok(RemoveNameOutcome::NoOp);
}
if obj.is_empty() {
fs::remove_file(meta_path)?;
Ok(RemoveNameOutcome::FileDeleted)
} else {
let content = crate::json::to_vec_pretty_sorted(&serde_json::Value::Object(obj))?;
fs::write(meta_path, &content)?;
Ok(RemoveNameOutcome::FieldRemoved)
}
}