use serde::{Serialize, de::DeserializeOwned};
pub type Extra = serde_json::Map<String, serde_json::Value>;
pub fn split_extra<T>(value: serde_json::Value) -> Result<(T, Extra), serde_json::Error>
where
T: Serialize + DeserializeOwned,
{
let typed: T = serde_json::from_value(value.clone())?;
let serde_json::Value::Object(mut extra) = value else {
return Ok((typed, Extra::new()));
};
if let serde_json::Value::Object(known) = serde_json::to_value(&typed)? {
for key in known.keys() {
extra.remove(key);
}
}
Ok((typed, extra))
}
pub fn merge_extra<T>(value: &T, extra: &Extra) -> Result<serde_json::Value, serde_json::Error>
where
T: Serialize,
{
let mut out = serde_json::to_value(value)?;
if let serde_json::Value::Object(map) = &mut out {
for (key, val) in extra {
map.entry(key.clone()).or_insert_with(|| val.clone());
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::*;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Inner {
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
}
#[test]
fn split_captures_only_unrecognized_keys() {
let value = serde_json::json!({"name": "vm", "tags": ["a"], "future": {"x": 1}});
let (inner, extra) = split_extra::<Inner>(value).unwrap();
assert_eq!(inner.name, "vm");
assert_eq!(extra.keys().collect::<Vec<_>>(), vec!["future"]);
}
#[test]
fn merge_restores_unrecognized_keys_without_duplicating_known_ones() {
let value = serde_json::json!({"name": "vm", "future": true});
let (inner, extra) = split_extra::<Inner>(value).unwrap();
let out = merge_extra(&inner, &extra).unwrap();
let obj = out.as_object().unwrap();
assert_eq!(obj.get("name").unwrap(), "vm");
assert_eq!(obj.get("future").unwrap(), &serde_json::json!(true));
assert_eq!(obj.len(), 2, "no stray or duplicated keys: {obj:?}");
}
#[test]
fn the_typed_value_wins_a_collision_with_a_stale_extra() {
let value = serde_json::json!({"name": "vm", "tags": []});
let (mut inner, extra) = split_extra::<Inner>(value).unwrap();
assert!(extra.contains_key("tags"));
inner.tags = vec!["edited".to_string()];
let out = merge_extra(&inner, &extra).unwrap();
assert_eq!(out["tags"], serde_json::json!(["edited"]));
}
#[test]
fn round_trip_is_stable_under_repetition() {
let original = serde_json::json!({"name": "vm", "future": {"nested": [1, 2]}});
let (inner, extra) = split_extra::<Inner>(original.clone()).unwrap();
let once = merge_extra(&inner, &extra).unwrap();
let (inner2, extra2) = split_extra::<Inner>(once.clone()).unwrap();
let twice = merge_extra(&inner2, &extra2).unwrap();
assert_eq!(once, twice, "round-tripping must reach a fixed point");
assert_eq!(once, original);
}
}
#[cfg(test)]
mod manifest_round_trip_tests {
use crate::manifest::VersionedManifest;
fn source() -> serde_json::Value {
serde_json::json!({
"manifest_version": "3.0.0",
"date": 1735689600,
"future_top_level": {"a": 1},
"channels": [{
"name": "0.15.0",
"future_channel_field": "keep me",
"components": [{
"name": "vm",
"version": {"kind": "registry", "version": "0.15.0"},
"kind": "executable",
"installation_method": {"kind": "cargo", "crate_name": "miden-vm"},
"installed-executable": "miden-vm",
"future_component_field": [1, 2, 3]
}]
}]
})
}
#[test]
fn unknown_fields_round_trip() {
let src = source();
let parsed = VersionedManifest::parse_str(&src.to_string()).expect("parse");
let out: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&parsed).unwrap()).unwrap();
assert_eq!(out["future_top_level"], src["future_top_level"]);
assert_eq!(out["channels"][0]["future_channel_field"], serde_json::json!("keep me"));
assert_eq!(
out["channels"][0]["components"][0]["future_component_field"],
serde_json::json!([1, 2, 3])
);
}
#[test]
fn known_fields_are_never_duplicated() {
let parsed = VersionedManifest::parse_str(&source().to_string()).expect("parse");
let text = serde_json::to_string(&parsed).unwrap();
for key in ["\"kind\"", "\"installed-executable\"", "\"name\"", "\"version\""] {
assert!(text.contains(key), "expected {key} in output");
}
let reparsed = VersionedManifest::parse_str(&text).expect("output must re-parse");
let again = serde_json::to_string(&reparsed).unwrap();
assert_eq!(text, again, "serialization must reach a fixed point");
let component = &serde_json::from_str::<serde_json::Value>(&text).unwrap()["channels"][0]
["components"][0];
let object = component.as_object().unwrap();
assert_eq!(object.get("kind").unwrap(), "executable");
assert_eq!(object.get("installed-executable").unwrap(), "miden-vm");
}
}