use serde_json::{Map, Value};
pub trait ValueObjectExt {
#[must_use = "an object-guard that isn't threaded via `?` swallows the underlying type mismatch"]
fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>>;
}
impl ValueObjectExt for Value {
#[inline]
fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>> {
self.as_object_mut()
.ok_or_else(|| anyhow::anyhow!("{slot} is not an object"))
}
}
pub trait JsonMapStrExt {
fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value>;
}
impl JsonMapStrExt for Map<String, Value> {
#[inline]
fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value> {
self.insert(key.into(), Value::String(value.into()))
}
}
pub trait JsonMapObjectEntryExt {
#[must_use = "an object-slot guard that isn't threaded via `?` swallows the underlying type mismatch"]
fn object_slot_mut_or(&mut self, slot: &'static str)
-> anyhow::Result<&mut Map<String, Value>>;
}
impl JsonMapObjectEntryExt for Map<String, Value> {
#[inline]
fn object_slot_mut_or(
&mut self,
slot: &'static str,
) -> anyhow::Result<&mut Map<String, Value>> {
self.entry(slot)
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut_or(slot)
}
}
pub trait ValueGetExt {
#[must_use = "a JSON i64 projection that isn't bound swallows the counter entirely"]
fn get_i64(&self, key: &str) -> Option<i64>;
#[must_use = "a JSON &str projection that isn't bound swallows the slot entirely"]
fn get_str(&self, key: &str) -> Option<&str>;
#[must_use = "a JSON array projection that isn't bound swallows the slot entirely"]
fn get_array(&self, key: &str) -> Option<&Vec<Value>>;
}
impl ValueGetExt for Value {
#[inline]
fn get_i64(&self, key: &str) -> Option<i64> {
self.get(key).and_then(Value::as_i64)
}
#[inline]
fn get_str(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Value::as_str)
}
#[inline]
fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
self.get(key).and_then(Value::as_array)
}
}
impl ValueGetExt for Map<String, Value> {
#[inline]
fn get_i64(&self, key: &str) -> Option<i64> {
self.get(key).and_then(Value::as_i64)
}
#[inline]
fn get_str(&self, key: &str) -> Option<&str> {
self.get(key).and_then(Value::as_str)
}
#[inline]
fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
self.get(key).and_then(Value::as_array)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn as_object_mut_or_object_arm_returns_the_inner_map_mutably() {
let mut v = json!({ "existing_key": "existing_value" });
let map = v.as_object_mut_or("resource").expect("Value::Object");
map.insert("new_key".to_string(), json!("new_value"));
assert_eq!(v["existing_key"], "existing_value");
assert_eq!(v["new_key"], "new_value");
}
#[test]
fn as_object_mut_or_null_arm_errors_with_pre_lift_display_bytewise() {
let mut v = Value::Null;
let err = v.as_object_mut_or("resource").unwrap_err();
assert_eq!(format!("{err}"), "resource is not an object");
}
#[test]
fn as_object_mut_or_array_arm_errors_with_pre_lift_display_bytewise() {
let mut v = json!(["not", "an", "object"]);
let err = v.as_object_mut_or("metadata").unwrap_err();
assert_eq!(format!("{err}"), "metadata is not an object");
}
#[test]
fn as_object_mut_or_string_arm_errors_with_pre_lift_display_bytewise() {
let mut v = json!("stringified");
let err = v.as_object_mut_or("annotations").unwrap_err();
assert_eq!(format!("{err}"), "annotations is not an object");
}
#[test]
fn as_object_mut_or_threads_the_slot_slug_verbatim_across_all_three_pre_lift_labels() {
for slot in ["resource", "metadata", "annotations"] {
let mut v = Value::Null;
let err = v.as_object_mut_or(slot).unwrap_err();
assert_eq!(format!("{err}"), format!("{slot} is not an object"));
}
}
#[test]
fn as_object_mut_or_object_arm_matches_inherent_as_object_mut_bytewise() {
let mut via_trait = json!({ "key": "value", "nested": { "inner": 1 } });
let mut via_inherent = via_trait.clone();
assert_eq!(
via_trait
.as_object_mut_or("resource")
.expect("Value::Object")
.clone(),
via_inherent.as_object_mut().expect("Value::Object").clone(),
);
}
#[test]
fn insert_str_new_key_returns_none_and_stamps_value_string() {
let mut m = Map::new();
let prev = m.insert_str("key", "value");
assert!(prev.is_none(), "new key returns None");
assert_eq!(m.get("key"), Some(&Value::String("value".to_string())));
assert!(matches!(m.get("key"), Some(Value::String(_))));
}
#[test]
fn insert_str_overwrite_returns_prior_value_and_stamps_new() {
let mut m = Map::new();
m.insert_str("key", "old");
let prev = m.insert_str("key", "new");
assert_eq!(prev, Some(Value::String("old".to_string())));
assert_eq!(m.get("key"), Some(&Value::String("new".to_string())));
}
#[test]
fn insert_str_accepts_str_and_owned_string_at_both_slots() {
let mut m1 = Map::new();
m1.insert_str("a", "b");
let mut m2 = Map::new();
m2.insert_str(String::from("a"), String::from("b"));
let mut m3 = Map::new();
m3.insert_str("a", String::from("b"));
let mut m4 = Map::new();
m4.insert_str(String::from("a"), "b");
assert_eq!(m1, m2);
assert_eq!(m2, m3);
assert_eq!(m3, m4);
}
#[test]
fn insert_str_matches_pre_lift_hand_authored_shape_bytewise() {
for (k_str, v_str) in [("a", "b"), ("x", ""), ("", "y"), ("", "")] {
let mut via_primitive = Map::new();
via_primitive.insert_str(k_str, v_str);
let mut via_pre_lift = Map::new();
via_pre_lift.insert(k_str.into(), Value::String(v_str.into()));
assert_eq!(via_primitive, via_pre_lift);
let mut via_primitive = Map::new();
via_primitive.insert_str(String::from(k_str), String::from(v_str));
let mut via_pre_lift = Map::new();
via_pre_lift.insert(String::from(k_str), Value::String(String::from(v_str)));
assert_eq!(via_primitive, via_pre_lift);
}
}
#[test]
fn insert_str_empty_value_stamps_empty_string_not_null() {
let mut m = Map::new();
m.insert_str("empty", "");
assert_eq!(m.get("empty"), Some(&Value::String(String::new())));
assert!(!matches!(m.get("empty"), Some(Value::Null)));
}
#[test]
fn object_slot_mut_or_absent_slot_seeds_empty_object_and_returns_it() {
let mut parent = Map::new();
{
let child = parent
.object_slot_mut_or("metadata")
.expect("absent slot seeds an object");
assert!(child.is_empty(), "fresh-seeded slot is an empty object");
child.insert("name".into(), Value::String("demo".into()));
}
assert_eq!(parent["metadata"]["name"], "demo");
assert!(matches!(parent.get("metadata"), Some(Value::Object(_))));
}
#[test]
fn object_slot_mut_or_present_object_slot_returns_existing_interior_mutably() {
let mut parent = Map::new();
parent.insert(
"metadata".into(),
serde_json::json!({ "existing_key": "existing_value" }),
);
{
let child = parent
.object_slot_mut_or("metadata")
.expect("present-object slot returns Ok");
assert_eq!(
child.get("existing_key"),
Some(&Value::String("existing_value".into()))
);
child.insert("new_key".into(), Value::String("new_value".into()));
}
assert_eq!(parent["metadata"]["existing_key"], "existing_value");
assert_eq!(parent["metadata"]["new_key"], "new_value");
}
#[test]
fn object_slot_mut_or_present_non_object_slot_errors_with_pre_lift_display() {
let mut parent = Map::new();
parent.insert("metadata".into(), Value::String("malformed".into()));
let err = parent.object_slot_mut_or("metadata").unwrap_err();
assert_eq!(format!("{err}"), "metadata is not an object");
}
#[test]
fn object_slot_mut_or_threads_the_slot_slug_verbatim_across_both_pre_lift_labels() {
for slot in ["metadata", "annotations"] {
let mut parent = Map::new();
parent.insert(slot.into(), Value::Null);
let err = parent.object_slot_mut_or(slot).unwrap_err();
assert_eq!(format!("{err}"), format!("{slot} is not an object"));
}
}
#[test]
fn object_slot_mut_or_present_empty_object_returns_existing_reference_not_synthesized() {
let mut parent = Map::new();
parent.insert("metadata".into(), Value::Object(Map::new()));
let addr_before = parent.get("metadata").unwrap() as *const Value;
{
let _child = parent.object_slot_mut_or("metadata").unwrap();
}
let addr_after = parent.get("metadata").unwrap() as *const Value;
assert_eq!(
addr_before, addr_after,
"present empty-object slot must return the pre-existing reference, not a fresh synthesis",
);
}
#[test]
fn object_slot_mut_or_matches_pre_lift_hand_authored_compound_shape_bytewise() {
for slot in ["metadata", "annotations"] {
let mut via_primitive = Map::new();
let mut via_pre_lift = Map::new();
{
let _ = via_primitive.object_slot_mut_or(slot).unwrap();
let _ = via_pre_lift
.entry(slot.to_string())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut_or(slot)
.unwrap();
}
assert_eq!(via_primitive, via_pre_lift);
let mut via_primitive = Map::new();
via_primitive.insert(slot.into(), serde_json::json!({ "k": "v" }));
let mut via_pre_lift = via_primitive.clone();
{
let a = via_primitive.object_slot_mut_or(slot).unwrap();
let b = via_pre_lift
.entry(slot.to_string())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut_or(slot)
.unwrap();
assert_eq!(a, b);
}
let mut via_primitive = Map::new();
via_primitive.insert(slot.into(), Value::Bool(true));
let mut via_pre_lift = via_primitive.clone();
let err_primitive = via_primitive.object_slot_mut_or(slot).unwrap_err();
let err_pre_lift = via_pre_lift
.entry(slot.to_string())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut_or(slot)
.unwrap_err();
assert_eq!(format!("{err_primitive}"), format!("{err_pre_lift}"));
}
}
#[test]
fn get_i64_present_integer_slot_returns_the_value() {
let status = json!({ "succeeded": 1, "failed": 3, "active": 5 });
assert_eq!(status.get_i64("succeeded"), Some(1));
assert_eq!(status.get_i64("failed"), Some(3));
assert_eq!(status.get_i64("active"), Some(5));
}
#[test]
fn get_i64_absent_slot_returns_none() {
let status = json!({});
assert_eq!(status.get_i64("succeeded"), None);
assert_eq!(status.get_i64("any_missing_key"), None);
}
#[test]
fn get_i64_present_but_non_integer_slot_returns_none() {
let status = json!({
"stringy": "1",
"boolean": true,
"object": {},
"array": [],
"null_valued": null,
});
assert_eq!(status.get_i64("stringy"), None);
assert_eq!(status.get_i64("boolean"), None);
assert_eq!(status.get_i64("object"), None);
assert_eq!(status.get_i64("array"), None);
assert_eq!(status.get_i64("null_valued"), None);
}
#[test]
fn get_i64_negative_counter_survives_the_projection() {
let status = json!({ "n": -1 });
assert_eq!(status.get_i64("n"), Some(-1));
}
#[test]
fn get_i64_non_object_receiver_returns_none_verbatim() {
assert_eq!(Value::Null.get_i64("any"), None);
assert_eq!(Value::Bool(true).get_i64("any"), None);
assert_eq!(json!([1, 2, 3]).get_i64("any"), None);
assert_eq!(json!("scalar").get_i64("any"), None);
}
#[test]
fn get_i64_matches_pre_lift_hand_authored_chain_shape() {
let status = json!({
"succeeded": 2,
"failed": 0,
"active": 7,
"stringy": "1",
"null_valued": null,
});
for key in [
"succeeded",
"failed",
"active",
"stringy",
"null_valued",
"missing",
] {
let via_primitive = status.get_i64(key);
let via_pre_lift = status.get(key).and_then(|v| v.as_i64());
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` must round-trip through both shapes",
);
}
}
#[test]
fn get_i64_composes_with_unwrap_or_default_at_default_seed_shape() {
let status = json!({ "succeeded": 4 });
assert_eq!(status.get_i64("missing").unwrap_or_default(), 0_i64);
assert_eq!(status.get_i64("succeeded").unwrap_or_default(), 4_i64);
}
#[test]
fn get_str_present_string_slot_returns_the_slice() {
let manifest = json!({
"apiVersion": "helm.toolkit.fluxcd.io/v2",
"kind": "HelmRelease",
"name": "demo-app",
"namespace": "demo",
});
assert_eq!(
manifest.get_str("apiVersion"),
Some("helm.toolkit.fluxcd.io/v2"),
);
assert_eq!(manifest.get_str("kind"), Some("HelmRelease"));
assert_eq!(manifest.get_str("name"), Some("demo-app"));
assert_eq!(manifest.get_str("namespace"), Some("demo"));
}
#[test]
fn get_str_absent_slot_returns_none() {
let manifest = json!({ "kind": "HelmRelease" });
assert_eq!(manifest.get_str("apiVersion"), None);
assert_eq!(manifest.get_str("any_missing_key"), None);
}
#[test]
fn get_str_present_but_non_string_slot_returns_none() {
let manifest = json!({
"numeric": 1,
"boolean": true,
"object": {},
"array": [],
"null_valued": null,
});
assert_eq!(manifest.get_str("numeric"), None);
assert_eq!(manifest.get_str("boolean"), None);
assert_eq!(manifest.get_str("object"), None);
assert_eq!(manifest.get_str("array"), None);
assert_eq!(manifest.get_str("null_valued"), None);
}
#[test]
fn get_str_empty_string_slot_survives_the_projection() {
let manifest = json!({ "namespace": "" });
assert_eq!(manifest.get_str("namespace"), Some(""));
}
#[test]
fn get_str_non_object_receiver_returns_none_verbatim() {
assert_eq!(Value::Null.get_str("any"), None);
assert_eq!(Value::Bool(true).get_str("any"), None);
assert_eq!(json!([1, 2, 3]).get_str("any"), None);
assert_eq!(json!("scalar").get_str("any"), None);
}
#[test]
fn get_str_matches_pre_lift_hand_authored_chain_shape() {
let manifest = json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"type": "Ready",
"numeric": 1,
"null_valued": null,
});
for key in [
"apiVersion",
"kind",
"type",
"numeric",
"null_valued",
"missing",
] {
let via_primitive = manifest.get_str(key);
let via_pre_lift = manifest.get(key).and_then(|v| v.as_str());
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` must round-trip through both shapes",
);
}
}
#[test]
fn get_str_composes_with_ok_or_else_at_from_json_shape() {
let manifest = json!({ "apiVersion": "v1" });
let ok_arm: String = manifest
.get_str("apiVersion")
.ok_or_else(|| anyhow::anyhow!("missing"))
.unwrap()
.to_string();
assert_eq!(ok_arm, "v1");
let err_arm = manifest
.get_str("kind")
.ok_or_else(|| anyhow::anyhow!("rendered resource missing kind"))
.unwrap_err();
assert_eq!(format!("{err_arm}"), "rendered resource missing kind");
let opt_present: Option<String> = manifest.get_str("apiVersion").map(str::to_string);
assert_eq!(opt_present.as_deref(), Some("v1"));
let opt_absent: Option<String> = manifest.get_str("kind").map(String::from);
assert!(opt_absent.is_none());
}
#[test]
fn get_str_return_lifetime_borrows_receiver_not_owned() {
let manifest = json!({ "apiVersion": "helm.toolkit.fluxcd.io/v2" });
let s: &str = manifest.get_str("apiVersion").unwrap();
let raw: &str = manifest.get("apiVersion").and_then(|v| v.as_str()).unwrap();
assert!(std::ptr::eq(s.as_ptr(), raw.as_ptr()));
}
#[test]
fn get_str_axis_family_reaches_i64_and_str_through_one_trait_import() {
fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>) {
(t.get_i64("n"), t.get_str("s"))
}
let mixed = json!({ "n": 7, "s": "hello" });
let (n, s) = probe(&mixed);
assert_eq!(n, Some(7));
assert_eq!(s, Some("hello"));
}
#[test]
fn get_array_present_array_slot_returns_the_slice() {
let status = json!({
"conditions": [
{ "type": "Ready", "status": "True" },
{ "type": "Progressing", "status": "False" },
],
});
let via = status.get_array("conditions").expect("Value::Array");
assert_eq!(via.len(), 2);
assert_eq!(via[0]["type"], "Ready");
let jwks = json!({
"keys": [
{ "kty": "RSA", "kid": "1" },
{ "kty": "RSA", "kid": "2" },
{ "kty": "EC", "kid": "3" },
],
});
assert_eq!(
jwks.get_array("keys").map(Vec::len),
Some(3),
"probe count_jwks_keys composition must reach the same tail as pre-lift",
);
}
#[test]
fn get_array_absent_slot_returns_none() {
let status = json!({});
assert_eq!(status.get_array("conditions"), None);
assert_eq!(status.get_array("any_missing_key"), None);
}
#[test]
fn get_array_present_but_non_array_slot_returns_none() {
let status = json!({
"stringy": "ready",
"numeric": 1,
"boolean": true,
"object": { "nested": true },
"null_valued": null,
});
assert_eq!(status.get_array("stringy"), None);
assert_eq!(status.get_array("numeric"), None);
assert_eq!(status.get_array("boolean"), None);
assert_eq!(status.get_array("object"), None);
assert_eq!(status.get_array("null_valued"), None);
}
#[test]
fn get_array_empty_array_slot_survives_the_projection() {
let jwks = json!({ "keys": [] });
let arr = jwks.get_array("keys").expect("Value::Array");
assert!(arr.is_empty());
assert_eq!(jwks.get_array("keys").map(Vec::len), Some(0));
}
#[test]
fn get_array_non_object_receiver_returns_none_verbatim() {
assert_eq!(Value::Null.get_array("any"), None);
assert_eq!(Value::Bool(true).get_array("any"), None);
assert_eq!(json!([1, 2, 3]).get_array("any"), None);
assert_eq!(json!("scalar").get_array("any"), None);
}
#[test]
fn get_array_matches_pre_lift_hand_authored_chain_shape() {
let manifest = json!({
"conditions": [{ "type": "Ready" }],
"keys": [{ "kid": "1" }, { "kid": "2" }],
"empty": [],
"stringy": "not-an-array",
"null_valued": null,
});
for key in [
"conditions",
"keys",
"empty",
"stringy",
"null_valued",
"missing",
] {
let via_primitive = manifest.get_array(key);
let via_pre_lift = manifest.get(key).and_then(|v| v.as_array());
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` must round-trip through both shapes",
);
}
}
#[test]
fn get_array_composes_with_len_map_at_probe_count_jwks_keys_shape() {
let jwks = json!({ "keys": [{ "kid": "1" }, { "kid": "2" }, { "kid": "3" }] });
let n: u64 = jwks
.get_array("keys")
.map(|xs| xs.len() as u64)
.unwrap_or(0);
assert_eq!(n, 3);
let empty = json!({});
let z: u64 = empty
.get_array("keys")
.map(|xs| xs.len() as u64)
.unwrap_or(0);
assert_eq!(z, 0);
}
#[test]
fn get_array_composes_with_let_else_short_circuit_at_ready_condition_shape() {
let data = json!({
"status": {
"conditions": [
{ "type": "Ready", "status": "True" },
{ "type": "Progressing", "status": "False" },
],
},
});
let conditions = data
.get("status")
.and_then(|s| s.get_array("conditions"))
.expect("nested walk resolves");
assert_eq!(conditions.len(), 2);
let types: Vec<&str> = conditions
.iter()
.filter_map(|c| c.get_str("type"))
.collect();
assert_eq!(types, vec!["Ready", "Progressing"]);
}
#[test]
fn get_array_return_lifetime_borrows_receiver_not_owned() {
let manifest = json!({ "keys": [{ "kid": "1" }, { "kid": "2" }] });
let via_primitive: &Vec<Value> = manifest.get_array("keys").unwrap();
let via_raw: &Vec<Value> = manifest.get("keys").and_then(|v| v.as_array()).unwrap();
assert!(std::ptr::eq(via_primitive.as_ptr(), via_raw.as_ptr()));
}
#[test]
fn get_array_axis_family_reaches_i64_str_and_array_through_one_trait_import() {
fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>, Option<&Vec<Value>>) {
(t.get_i64("n"), t.get_str("s"), t.get_array("a"))
}
let mixed = json!({ "n": 7, "s": "hello", "a": [1, 2, 3] });
let (n, s, a) = probe(&mixed);
assert_eq!(n, Some(7));
assert_eq!(s, Some("hello"));
assert_eq!(a.map(Vec::len), Some(3));
}
#[test]
fn map_receiver_reaches_str_i64_and_array_axes_through_the_same_trait() {
let obj: Map<String, Value> = json!({
"s": "hello",
"n": 42,
"a": [1, 2, 3],
})
.as_object()
.unwrap()
.clone();
assert_eq!(obj.get_str("s"), Some("hello"));
assert_eq!(obj.get_i64("n"), Some(42));
assert_eq!(obj.get_array("a").map(Vec::len), Some(3));
}
#[test]
fn map_receiver_get_str_matches_pre_lift_hand_authored_chain_bytewise() {
let obj: Map<String, Value> = json!({
"phase": "Running",
"phaseSince": "2026-01-01T00:00:00Z",
"message": "",
"numeric": 7,
"null_valued": null,
})
.as_object()
.unwrap()
.clone();
for key in [
"phase",
"phaseSince",
"message",
"numeric",
"null_valued",
"missing",
] {
let via_primitive = obj.get_str(key);
let via_pre_lift = obj.get(key).and_then(Value::as_str);
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` on Map receiver must round-trip through both shapes",
);
}
}
#[test]
fn map_receiver_get_array_matches_pre_lift_hand_authored_chain_bytewise() {
let obj: Map<String, Value> = json!({
"finalizers": ["tatara.pleme.io/process-finalizer", "other.io/finalizer"],
"fluxResources": [],
"stringy": "not-array",
})
.as_object()
.unwrap()
.clone();
for key in ["finalizers", "fluxResources", "stringy", "missing"] {
let via_primitive = obj.get_array(key);
let via_pre_lift = obj.get(key).and_then(Value::as_array);
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` on Map receiver's array axis must round-trip through both shapes",
);
}
}
#[test]
fn map_receiver_get_i64_matches_pre_lift_hand_authored_chain_bytewise() {
let obj: Map<String, Value> = json!({
"succeeded": 2,
"failed": 0,
"active": 5,
"stringy": "1",
"null_valued": null,
})
.as_object()
.unwrap()
.clone();
for key in [
"succeeded",
"failed",
"active",
"stringy",
"null_valued",
"missing",
] {
let via_primitive = obj.get_i64(key);
let via_pre_lift = obj.get(key).and_then(Value::as_i64);
assert_eq!(
via_primitive, via_pre_lift,
"corner `{key}` on Map receiver's integer axis must round-trip through both shapes",
);
}
}
#[test]
fn map_receiver_get_str_matches_value_object_arm_bytewise() {
let v: Value = json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"phase": "Running",
});
let m: &Map<String, Value> = v.as_object().unwrap();
for key in ["apiVersion", "kind", "phase", "missing"] {
assert_eq!(
<Map<String, Value> as ValueGetExt>::get_str(m, key),
<Value as ValueGetExt>::get_str(&v, key),
"receiver-shape parity: `{key}` must project identically through both impls",
);
}
}
#[test]
fn map_receiver_axis_family_reaches_all_three_axes_through_one_trait_import() {
fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>, Option<&Vec<Value>>) {
(t.get_i64("n"), t.get_str("s"), t.get_array("a"))
}
let m: Map<String, Value> = json!({ "n": 7, "s": "hello", "a": [1, 2, 3] })
.as_object()
.unwrap()
.clone();
let (n, s, a) = probe(&m);
assert_eq!(n, Some(7));
assert_eq!(s, Some("hello"));
assert_eq!(a.map(Vec::len), Some(3));
}
}