mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Sorted keys
This commit is contained in:
parent
d08780fc14
commit
929bc254fa
16 changed files with 145 additions and 36 deletions
|
|
@ -3,23 +3,23 @@ source: tests/tests/syncback.rs
|
|||
expression: container.model.json
|
||||
---
|
||||
{
|
||||
"className": "Folder",
|
||||
"children": [
|
||||
{
|
||||
"name": "value_1",
|
||||
"className": "ObjectValue",
|
||||
"attributes": {
|
||||
"Rojo_Id": "value_1",
|
||||
"Rojo_Target_Value": "value_1"
|
||||
}
|
||||
},
|
||||
"className": "ObjectValue",
|
||||
"name": "value_1"
|
||||
},
|
||||
{
|
||||
"name": "value_2",
|
||||
"className": "ObjectValue",
|
||||
"attributes": {
|
||||
"Rojo_Id": "72bc28150ada2e6206442ee300004084",
|
||||
"Rojo_Target_Value": "72bc28150ada2e6206442ee300004084"
|
||||
},
|
||||
"className": "ObjectValue",
|
||||
"name": "value_2"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"className": "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ source: tests/tests/syncback.rs
|
|||
expression: src/dir_with_meta/init.meta.json
|
||||
---
|
||||
{
|
||||
"className": "Configuration",
|
||||
"properties": {
|
||||
"Tags": [
|
||||
"This tag on dir_with_meta"
|
||||
]
|
||||
},
|
||||
"className": "Configuration"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ source: tests/tests/syncback.rs
|
|||
expression: src/pointer.model.json
|
||||
---
|
||||
{
|
||||
"className": "ObjectValue",
|
||||
"attributes": {
|
||||
"Rojo_Target_Value": "test referent id"
|
||||
}
|
||||
},
|
||||
"className": "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ source: tests/tests/syncback.rs
|
|||
expression: src/target.model.json
|
||||
---
|
||||
{
|
||||
"className": "Folder",
|
||||
"attributes": {
|
||||
"Rojo_Id": "test referent id"
|
||||
}
|
||||
},
|
||||
"className": "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ source: tests/tests/syncback.rs
|
|||
expression: src/pointer.model.json
|
||||
---
|
||||
{
|
||||
"className": "ObjectValue",
|
||||
"attributes": {
|
||||
"Rojo_Target_Value": "62e89c49e4f800c20629c71b00003fc0"
|
||||
}
|
||||
},
|
||||
"className": "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
source: tests/tests/syncback.rs
|
||||
assertion_line: 28
|
||||
expression: src/Pointer_2.model.json
|
||||
---
|
||||
{
|
||||
"className": "ObjectValue",
|
||||
"attributes": {
|
||||
"Rojo_Target_Value": "0bd3e9e11879191708a2bc4a00000c20"
|
||||
}
|
||||
},
|
||||
"className": "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
source: tests/tests/syncback.rs
|
||||
assertion_line: 28
|
||||
expression: src/Target_2.model.json
|
||||
---
|
||||
{
|
||||
"className": "Folder",
|
||||
"attributes": {
|
||||
"Rojo_Id": "0bd3e9e11879191708a2bc4a00000c20"
|
||||
}
|
||||
},
|
||||
"className": "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ expression: default.project.json
|
|||
{
|
||||
"name": "respect_old_middleware",
|
||||
"tree": {
|
||||
"$path": "src",
|
||||
"project_node": {
|
||||
"$className": "BoolValue",
|
||||
"$properties": {
|
||||
"Value": true
|
||||
}
|
||||
},
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ expression: default.project.json
|
|||
---
|
||||
{
|
||||
"name": "unscriptable_properties",
|
||||
"syncbackRules": {
|
||||
"syncUnscriptable": true
|
||||
},
|
||||
"tree": {
|
||||
"$className": "BinaryStringValue",
|
||||
"$properties": {
|
||||
|
|
@ -11,8 +14,5 @@ expression: default.project.json
|
|||
"BinaryString": "Rojo/is/cool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"syncbackRules": {
|
||||
"syncUnscriptable": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
src/json.rs
112
src/json.rs
|
|
@ -4,7 +4,7 @@
|
|||
//! to reduce boilerplate and improve ergonomics when working with JSONC files.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
/// Parse JSONC text into a `serde_json::Value`.
|
||||
///
|
||||
|
|
@ -131,6 +131,55 @@ pub fn from_slice_with_context<T: DeserializeOwned>(
|
|||
from_str_with_context(text, context)
|
||||
}
|
||||
|
||||
/// Recursively sorts all object keys in a JSON value for deterministic output.
|
||||
fn sort_json_value(value: serde_json::Value) -> serde_json::Value {
|
||||
match value {
|
||||
serde_json::Value::Object(map) => {
|
||||
let sorted: serde_json::Map<String, serde_json::Value> = map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, sort_json_value(v)))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.collect::<std::collections::BTreeMap<_, _>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
serde_json::Value::Object(sorted)
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
serde_json::Value::Array(arr.into_iter().map(sort_json_value).collect())
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a value to a pretty-printed JSON byte vector with sorted keys.
|
||||
///
|
||||
/// This ensures deterministic output by sorting all object keys alphabetically,
|
||||
/// which is especially useful for version control and syncback operations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the value cannot be serialized to JSON.
|
||||
pub fn to_vec_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<Vec<u8>> {
|
||||
let json_value = serde_json::to_value(value).context("Failed to convert to JSON value")?;
|
||||
let sorted_value = sort_json_value(json_value);
|
||||
serde_json::to_vec_pretty(&sorted_value).context("Failed to serialize to JSON")
|
||||
}
|
||||
|
||||
/// Serialize a value to a pretty-printed JSON string with sorted keys.
|
||||
///
|
||||
/// This ensures deterministic output by sorting all object keys alphabetically,
|
||||
/// which is especially useful for version control and syncback operations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the value cannot be serialized to JSON.
|
||||
pub fn to_string_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<String> {
|
||||
let json_value = serde_json::to_value(value).context("Failed to convert to JSON value")?;
|
||||
let sorted_value = sort_json_value(json_value);
|
||||
serde_json::to_string_pretty(&sorted_value).context("Failed to serialize to JSON")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -310,4 +359,65 @@ mod tests {
|
|||
assert!(err.to_string().contains("config.json"));
|
||||
assert!(err.to_string().contains("UTF-8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_vec_pretty_sorted() {
|
||||
use indexmap::IndexMap;
|
||||
|
||||
let mut map: IndexMap<String, i32> = IndexMap::new();
|
||||
map.insert("zebra".to_string(), 1);
|
||||
map.insert("apple".to_string(), 2);
|
||||
map.insert("mango".to_string(), 3);
|
||||
|
||||
let result = to_vec_pretty_sorted(&map).unwrap();
|
||||
let output = String::from_utf8(result).unwrap();
|
||||
|
||||
// Keys should be in alphabetical order
|
||||
let apple_pos = output.find("apple").unwrap();
|
||||
let mango_pos = output.find("mango").unwrap();
|
||||
let zebra_pos = output.find("zebra").unwrap();
|
||||
assert!(apple_pos < mango_pos);
|
||||
assert!(mango_pos < zebra_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_pretty_sorted() {
|
||||
use indexmap::IndexMap;
|
||||
|
||||
let mut map: IndexMap<String, i32> = IndexMap::new();
|
||||
map.insert("zebra".to_string(), 1);
|
||||
map.insert("apple".to_string(), 2);
|
||||
map.insert("mango".to_string(), 3);
|
||||
|
||||
let output = to_string_pretty_sorted(&map).unwrap();
|
||||
|
||||
// Keys should be in alphabetical order
|
||||
let apple_pos = output.find("apple").unwrap();
|
||||
let mango_pos = output.find("mango").unwrap();
|
||||
let zebra_pos = output.find("zebra").unwrap();
|
||||
assert!(apple_pos < mango_pos);
|
||||
assert!(mango_pos < zebra_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorted_nested_objects() {
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Nested {
|
||||
inner: std::collections::HashMap<String, i32>,
|
||||
}
|
||||
|
||||
let mut inner = std::collections::HashMap::new();
|
||||
inner.insert("z".to_string(), 1);
|
||||
inner.insert("a".to_string(), 2);
|
||||
|
||||
let nested = Nested { inner };
|
||||
let output = to_string_pretty_sorted(&nested).unwrap();
|
||||
|
||||
// Inner keys should also be sorted
|
||||
let a_pos = output.find("\"a\"").unwrap();
|
||||
let z_pos = output.find("\"z\"").unwrap();
|
||||
assert!(a_pos < z_pos);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub fn syncback_csv<'sync>(
|
|||
let parent = snapshot.path.parent_err()?;
|
||||
fs_snapshot.add_file(
|
||||
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||
crate::json::to_vec_pretty_sorted(&meta).context("cannot serialize metadata")?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ pub fn syncback_csv_init<'sync>(
|
|||
if !meta.is_empty() {
|
||||
dir_syncback.fs_snapshot.add_file(
|
||||
snapshot.path.join("init.meta.json"),
|
||||
serde_json::to_vec_pretty(&meta)
|
||||
crate::json::to_vec_pretty_sorted(&meta)
|
||||
.context("could not serialize new init.meta.json")?,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ pub fn syncback_dir<'sync>(
|
|||
if !meta.is_empty() {
|
||||
dir_syncback.fs_snapshot.add_file(
|
||||
snapshot.path.join("init.meta.json"),
|
||||
serde_json::to_vec_pretty(&meta)
|
||||
crate::json::to_vec_pretty_sorted(&meta)
|
||||
.context("could not serialize new init.meta.json")?,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ pub fn syncback_json_model<'sync>(
|
|||
Ok(SyncbackReturn {
|
||||
fs_snapshot: FsSnapshot::new().with_added_file(
|
||||
&snapshot.path,
|
||||
serde_json::to_vec_pretty(&model).context("failed to serialize new JSON Model")?,
|
||||
crate::json::to_vec_pretty_sorted(&model)
|
||||
.context("failed to serialize new JSON Model")?,
|
||||
),
|
||||
children: Vec::new(),
|
||||
removed_children: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ pub fn syncback_lua<'sync>(
|
|||
let parent_location = snapshot.path.parent_err()?;
|
||||
fs_snapshot.add_file(
|
||||
parent_location.join(format!("{}.meta.json", new_inst.name)),
|
||||
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
||||
crate::json::to_vec_pretty_sorted(&meta).context("cannot serialize metadata")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -203,7 +203,7 @@ pub fn syncback_lua_init<'sync>(
|
|||
if !meta.is_empty() {
|
||||
dir_syncback.fs_snapshot.add_file(
|
||||
snapshot.path.join("init.meta.json"),
|
||||
serde_json::to_vec_pretty(&meta)
|
||||
crate::json::to_vec_pretty_sorted(&meta)
|
||||
.context("could not serialize new init.meta.json")?,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ pub fn syncback_project<'sync>(
|
|||
|
||||
for (node_properties, node_attributes, old_inst) in node_changed_map {
|
||||
if project_node_should_reserialize(node_properties, node_attributes, old_inst)? {
|
||||
fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?);
|
||||
fs_snapshot.add_file(project_path, crate::json::to_vec_pretty_sorted(&project)?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ pub fn syncback_txt<'sync>(
|
|||
let parent = snapshot.path.parent_err()?;
|
||||
fs_snapshot.add_file(
|
||||
parent.join(format!("{}.meta.json", new_inst.name)),
|
||||
serde_json::to_vec_pretty(&meta).context("could not serialize metadata")?,
|
||||
crate::json::to_vec_pretty_sorted(&meta).context("could not serialize metadata")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue