Skip to main content

Module json_object

Module json_object 

Source
Expand description

Substrate primitive over serde_json::Value — the ONE substrate owner of the .as_object_mut().ok_or_else(|| anyhow::anyhow!( "<slot> is not an object")) guard-shape every JSON-mutating helper restates by hand at the “walk this Value slot into its serde_json::Map interior or fail loud” boundary.

Peer of the trait family that already lives in this crate on the wrap-shape axis:

  • crate::kube_error::KubeResultExt — the kube::Error → anyhow display-prefix wrap.
  • crate::hostname::HostnameResultExt — the HostnameError → anyhow display-prefix wrap.
  • crate::anyhow_flatten::FlattenCtxExt — the anyhow::Error → anyhow display-prefix flatten.
  • This module — the Option<&mut Map> → anyhow::Result<&mut Map> type-guard, partitioned from the three above by SOURCE (None from the slot-typecheck, not a lifted error type) but sharing the .map_err(|_| anyhow!("<slug>: …"))? display-prefix wire format. The module also owns the READ-side ValueGetExt projector (.get_i64(<key>) -> Option<i64>) — sibling of the three MUTATION-side traits below on the (read, mutate) axis, closing the READ half of the serde_json::Value substrate the four traits jointly own.

Pre-lift the shape was hand-authored at THREE adjacent private helpers in tatara-reconciler::ssapply past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold:

  • metadata_object_mut(resource) — the root-guard step (resource.as_object_mut().ok_or_else(|| anyhow!("resource is not an object"))?) that opens the SSA-time resource → &mut metadata walk shared by inject_owner_reference
    • inject_annotations.
  • metadata_object_mut(resource) — the metadata-slot type-check step (metadata.as_object_mut().ok_or_else(|| anyhow!("metadata is not an object"))?) that closes the same walk — a resource whose author mistyped the metadata slot as an array / string surfaces as an error rather than as a silent .as_object_mut() → None → skip no-op.
  • inject_annotations(resource, process) — the annotations-slot type-check step (annot.as_object_mut().ok_or_else(|| anyhow!( "annotations is not an object"))?) that opens the SSA-time metadata → &mut annotations walk before the ownership tag + observed-* primitive family drops its keys into the map.

All three restated the SAME 2-line shape verbatim: .as_object_mut() on a serde_json::Value handle already known to be non-null, then .ok_or_else(|| anyhow!("<slot-name> is not an object")) wrap whose slot name matched the walk step’s semantic role ("resource" / "metadata" / "annotations"). THREE byte-for-byte identical guard blocks past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold, differing only in the &'static str slot name each callsite stamped.

Post-lift each callsite reads <value>.as_object_mut_or("<slot>")? and the guard-shape lives at ONE substrate owner here. The composed anyhow::Error’s Display is byte-identical to the pre-lift chain ("<slot> is not an object"), so operator-facing log output and any error-chain greps still match bytewise. A regression that drifts the message (a "<slot> is not a JSON object" synonym, a swapped <slot> slot, a promotion to a chain-form source that only surfaces via the alternate {e:#} formatter) surfaces at the tests below rather than as silent operator-facing drift across the three pre-lift consumers.

§Naming — as_object_mut_or, not as_object_mut

Same discipline as the three sibling traits above — the trait method deliberately does NOT share a name with the inherent serde_json::Value::as_object_mut method (which returns Option<&mut Map>), because a name collision would let a caller who has ValueObjectExt in scope resolve to the inherent method by accident (inherent methods win over trait methods in method resolution) and silently drop the type-guard wrap altogether. The _or suffix names the intent: guard the Option → Result step at the same call, matching the pre-lift .as_object_mut(). ok_or_else(...) chain.

§#[must_use]

Every consumer threads the ? short-circuit onto its handler’s Result<_, anyhow::Error> return — dropping the guard swallows the underlying type-mismatch entirely, which is never the intended semantic at any of the three pre-lift consumers (each downstream md.entry(...).or_insert_with(...) / annot.insert(...) mutation depends on the returned &mut Map reference).

Theory anchor: THEORY.md §VI.1 (generation over composition — the .as_object_mut().ok_or_else(|| anyhow!("<slot> is not an object")) guard-shape recurred at three hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE substrate owner here). THEORY.md §II.1 invariant 5 (composition preserves proofs — a regression that drifts the guard message wording at ONE site surfaces here at the substrate pin rather than as silent operator-facing skew across every SSA-time metadata_object_mut + inject_annotations mutation).

Traits§

JsonMapObjectEntryExt
Substrate extension trait over serde_json::Map<String, Value> — the ONE substrate owner of the .entry(<key>).or_insert_with(|| Value::Object(<empty>)) seed-then-guard shape every JSON-mutating helper hand-authored at the “walk into this object slot on the parent map, seeding an empty object if the slot is absent, or fail loud if the slot exists but is a non-object” boundary.
JsonMapStrExt
Substrate extension trait over serde_json::Map<String, Value> — the ONE substrate owner of the map.insert(<key>.into(), Value::String(<val>.into())) string-slot insertion shape every JSON-mutating helper in the workspace hand-authored at each callsite.
ValueGetExt
Substrate extension trait over serde_json::Value — the ONE substrate owner of the paired .get(<key>).and_then(|v| v.as_<T>()) two-link READ chain every downstream projection walks to pull a typed leaf off a Kubernetes-status blob (or an equivalent rendered-resource JSON object) without asserting the slot is present, without asserting its variant, and without asserting the slot fits the target scalar type.
ValueObjectExt
Substrate extension trait over serde_json::Value — the ONE substrate owner of the .as_object_mut().ok_or_else(|| anyhow!( "<slot> is not an object")) guard-shape. See the module docs for the full callsite audit + the naming rationale (why as_object_mut_or and not as_object_mut).