Skip to main content

ValueGetExt

Trait ValueGetExt 

Source
pub trait ValueGetExt {
    // Required methods
    fn get_i64(&self, key: &str) -> Option<i64>;
    fn get_str(&self, key: &str) -> Option<&str>;
    fn get_array(&self, key: &str) -> Option<&Vec<Value>>;
}
Expand description

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.

The trait carries ONE method per typed READ axis; the axis-family is Self::get_i64 (integer counters) + Self::get_str (string slots) + Self::get_array (JSON array slots). Adding a new axis (a get_bool for Value::Bool, a get_object for Value::Object, a get_f64 for Value::Number truncated to f64) lands as ONE new method here + ONE impl arm, inheriting the naming, #[must_use], and inline discipline the existing axes pin. Never open a peer trait for a new axis — keep every READ projection on the ONE substrate owner so a caller who imports ValueGetExt reaches every axis through the same trait handle.

READ-side counterpart to the three MUTATION-side siblings already in this module — ValueObjectExt::as_object_mut_or, JsonMapStrExt::insert_str, JsonMapObjectEntryExt::object_slot_mut_or — partitioning the substrate along the (read, mutate) axis on the same serde_json::Value / serde_json::Map<String, Value> carrier pair.

Pre-lift the two-link chain was hand-authored at THREE adjacent slots inside tatara-reconciler::boundary::fetch_job_status, each projecting one batch/v1::Job status.<counter> field out of the fetched serde_json::Value object into a private JobStatusView row past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold:

  • status.get("succeeded").and_then(|v| v.as_i64()) — the Job-completion counter every JobAttested + ClosedLoopAuth postcondition evaluator gates on (succeeded < 1 short-circuits to Satisfaction::Unsatisfied("… still running (…)")).
  • status.get("failed").and_then(|v| v.as_i64()) — the Job-failure counter the same evaluators gate on (failed > 0 short-circuits to Satisfaction::Unsatisfied("… failed (status.failed={n})")).
  • status.get("active").and_then(|v| v.as_i64()) — the Job-in-flight counter the “still running” diagnostic tail reports as (succeeded={s}, active={a}).

All THREE sites walked the SAME two-link chain — .get(<key>) on a serde_json::Value already known to be the status object, then .and_then(|v| v.as_i64()) on the returned Option<&Value> — and each was followed by an if let Some(...) write into the [JobStatusView] row initialised from Default::default(). Post- lift each callsite reads status.get_i64(<key>) and the two-link READ chain lives at ONE substrate owner here.

§Naming — get_i64, not as_i64 or i64_at

Same discipline as the three sibling traits above — the trait method deliberately does NOT collide with serde_json::Value::as_i64 (the inherent projection on a single Value handle) nor with serde_json::Value::get (the inherent slot-lookup returning Option<&Value>). A name collision would let a caller who has ValueGetExt in scope resolve to one of the inherent methods by accident (inherent methods win over trait methods in method resolution) and silently drop half of the paired chain. The get_i64(<key>) shape names the intent: look up the slot at <key>, project the returned handle to i64, in ONE call.

§#[must_use]

Every consumer either binds the returned Option<i64> into a downstream if let Some(n) = ... / .unwrap_or_default() / struct- field construction. Dropping the return silently discards the projection entirely, which is never the intended semantic at the three pre-lift consumers (each downstream write depends on the returned counter).

§Composability

  • Key slot is &str — matches every pre-lift .get("<literal>") callsite and the inherent serde_json::Value::get’s primary str-index arm. A caller with a runtime-computed key (a String produced by a template composer) reaches through .get_i64(&s) mechanically via Deref<Target = str>.
  • Returns Option<i64> matching the composed inherent chain’s own return; a consumer wanting the “absent or non-integer → 0” fallback composes .unwrap_or_default() (or .unwrap_or(0)) at the callsite, keeping the “should this counter default to 0 or fail loud” decision at the caller rather than baking it into the primitive.
  • Non-object receivers (a Value::String, a Value::Null) return None verbatim via the inherent Value::get’s own non-object- arm behaviour, matching the pre-lift chain’s semantics on the corner where the caller’s status blob is malformed.

A future normalization — a per-fleet clamp that rejects negative counters (the K8s API server never emits them, but a fixture authoring bug could), a Value::Number fallback that accepts f64 counters truncated to i64, a checked overflow arm that promotes an out-of-range integer to a diagnostic rather than a silent None — lands at THIS ONE substrate primitive and every downstream Job-status / Deployment-replica / HPA-desired-count counter reader inherits the upgrade mechanically. No per-site edit at any of the 3 listed callers or at future consumers (a Deployment readyReplicas projection, an HPA currentReplicas gate, a StatefulSet updatedReplicas freshness check).

Theory anchor: THEORY.md §VI.1 (generation over composition — the two-link .get(<key>).and_then(|v| v.as_i64()) chain 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 drifted the projection axis at ONE site — a swap of as_i64 for as_u64 narrowing the accepted range, a swap of .get(<key>) for .pointer("<key>") losing the direct-child semantics — would silently pass every downstream JobStatusView composition and surface as a wrong counter at operator-facing diagnostic wording; post-lift the projection lives at ONE typed owner so a regression surfaces at [tests::get_i64_null_arm_returns_none] / peers rather than as silent operator-facing drift).

Required Methods§

Source

fn get_i64(&self, key: &str) -> Option<i64>

Look up key on this JSON object and project the returned handle to i64; returns None when the slot is absent, when the receiver is not a JSON object, or when the slot’s variant is not integer-shaped.

Source

fn get_str(&self, key: &str) -> Option<&str>

Look up key on this JSON object and project the returned handle to &str; returns None when the slot is absent, when the receiver is not a JSON object, or when the slot’s variant is not Value::String.

String-axis sibling of Self::get_i64 on the same .get(<key>).and_then(|v| v.as_<T>()) READ-chain lift. Pre-lift the two-link chain was hand-authored at SEVEN production sites across two crates past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold:

  • tatara-process::status::RenderedResourceCoords::from_json — FOUR paired reads (apiVersion, kind, metadata.name, metadata.namespace) that project the four rendered-resource coordinate slots off a serde_json::Value rendered manifest into the typed RenderedResourceCoords row; the required three (apiVersion / kind / metadata.name) compose with .ok_or_else(|| anyhow!("rendered resource missing X"))? .to_string(), and the optional metadata.namespace composes with .map(str::to_string).
  • tatara-reconciler::ssapply::ready_condition_value — THREE paired reads (type, status, message) inside the condition-walker’s per-condition classifier, each pulling a Value::String slot off a K8s Condition object off the status.conditions[] array.

All seven sites walked the SAME two-link chain — .get(<key>) on a serde_json::Value already known to be an object, then .and_then(|v| v.as_str()) on the returned Option<&Value> — and each composed different downstream tails (fallible .ok_or_else(...)?.to_string(), optional .map(String::from), pattern-match Some("True") / Some("False") / _). Post-lift each callsite reads <value>.get_str(<key>) and the two-link READ chain lives at ONE substrate owner here.

§Naming — get_str, not as_str or str_at

Same discipline as Self::get_i64 — the trait method deliberately does NOT collide with serde_json::Value::as_str (the inherent projection on a single Value handle) nor with serde_json::Value::get (the inherent slot-lookup returning Option<&Value>). A name collision would let a caller who has ValueGetExt in scope resolve to one of the inherent methods by accident (inherent methods win over trait methods in method resolution) and silently drop half of the paired chain. The get_str(<key>) shape names the intent: look up the slot at <key>, project the returned handle to &str, in ONE call.

§#[must_use]

Every pre-lift consumer binds the returned Option<&str> into a downstream .ok_or_else(...)?.to_string() / .map(String::from) / .map(str::to_string) / pattern-match arm. Dropping the return silently discards the projection entirely, which is never the intended semantic at any of the seven pre-lift consumers.

§Return lifetime

The &str borrows the same buffer the underlying Value::String variant owns; the Option<&str> is bounded by the receiver’s lifetime (&'_ self), so a caller holding onto the returned slice keeps the receiver borrowed. Matches the pre-lift chain’s own borrow shape (v.as_str() borrows through the &Value).

A future normalization on the projection — a Unicode normalization pass (NFC-folding annotation values), a per-fleet trim of leading/trailing whitespace, a rejection of empty-string arms as “the caller meant absent” — lands at THIS ONE substrate primitive and every downstream apiVersion / kind / metadata.name / K8s-condition-string reader inherits the upgrade mechanically.

Theory anchor: THEORY.md §VI.1 (generation over composition — the two-link .get(<key>).and_then(|v| v.as_str()) chain recurred at SEVEN production sites across two crates past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE substrate owner here on the string axis of the same READ-chain axis-family the get_i64 sibling opened for the integer axis). THEORY.md §II.1 invariant 5 (composition preserves proofs — a regression that drifted the projection axis at ONE site would silently pass every downstream composition and surface as a wrong slot at operator-facing diagnostic wording; post-lift the projection lives at ONE typed owner so a regression surfaces at [tests::get_str_present_string_slot_returns_the_slice] / peers rather than as silent operator-facing drift).

Source

fn get_array(&self, key: &str) -> Option<&Vec<Value>>

Look up key on this JSON object and project the returned handle to &Vec<Value>; returns None when the slot is absent, when the receiver is not a JSON object, or when the slot’s variant is not Value::Array.

Array-axis sibling of Self::get_i64 + Self::get_str on the same .get(<key>).and_then(|v| v.as_<T>()) READ-chain axis-family. Pre-lift the two-link chain was hand-authored at TWO production sites across two crates past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold:

  • tatara-reconciler::ssapply::ready_condition_value — the tail of the data.get("status").and_then(|s| s.get("conditions")).and_then(|c| c.as_array()) walker that opens the K8s Condition classifier every DynamicObject readiness probe rides through.
  • tatara-closed-loop-probe::probe::count_jwks_keys — the JWKS-response walker that counts issuer-side public keys off the keys slot for the closed-loop probe’s per-run jwks_key_count diagnostic.

Both sites walked the SAME two-link chain — .get(<key>) on a serde_json::Value already known to be an object, then .and_then(|v| v.as_array()) on the returned Option<&Value> — and composed different downstream tails (Some(conditions) pattern-match on the reconciler side, .map(|xs| xs.len() as u64) on the probe side). Post-lift each callsite reads <value>.get_array(<key>) and the two-link READ chain lives at ONE substrate owner here. The probe-side variant additionally sheds the pre-lift .get("keys").cloned() allocation because this primitive borrows through the receiver rather than cloning.

§Naming — get_array, not as_array or array_at

Same discipline as Self::get_i64 + Self::get_str — the trait method deliberately does NOT collide with serde_json::Value::as_array (the inherent projection on a single Value handle) nor with serde_json::Value::get (the inherent slot-lookup returning Option<&Value>). A name collision would let a caller who has ValueGetExt in scope resolve to one of the inherent methods by accident (inherent methods win over trait methods in method resolution) and silently drop half of the paired chain. The get_array(<key>) shape names the intent: look up the slot at <key>, project the returned handle to &Vec<Value>, in ONE call.

§#[must_use]

Every pre-lift consumer binds the returned Option<&Vec<Value>> into a downstream let Some(...) = ... else { return ... } short-circuit or a .map(|xs| xs.len() as u64).unwrap_or(0) counter composition. Dropping the return silently discards the projection entirely, which is never the intended semantic at either pre-lift consumer.

§Return lifetime

The &Vec<Value> borrows the same buffer the underlying Value::Array variant owns; the Option<&Vec<Value>> is bounded by the receiver’s lifetime (&'_ self), so a caller iterating the returned slice keeps the receiver borrowed. Matches the pre-lift chain’s own borrow shape (v.as_array() borrows through the &Value), and in the probe.rs case eliminates the pre-lift .cloned() on the intermediate Value that only existed to sidestep the borrow.

A future normalization on the projection — a rejection of empty arrays as “the caller meant absent”, an accept-scalar coercion (a Value::String promoted to a one-element array), a per-fleet cap on array length that short-circuits pathological payloads — lands at THIS ONE substrate primitive and every downstream K8s-Condition classifier / JWKS-array counter / future array-slot reader inherits the upgrade mechanically.

Theory anchor: THEORY.md §VI.1 (generation over composition — the two-link .get(<key>).and_then(|v| v.as_array()) chain recurred at two production sites across two crates past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE substrate owner here on the array axis of the same READ-chain axis-family the get_i64 + get_str siblings already own). THEORY.md §II.1 invariant 5 (composition preserves proofs — a regression that drifted the projection axis at ONE site would silently pass every downstream composition and surface as a wrong slot at operator-facing diagnostic wording; post-lift the projection lives at ONE typed owner so a regression surfaces at [tests::get_array_present_array_slot_returns_the_slice] / peers rather than as silent operator-facing drift).

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementations on Foreign Types§

Source§

impl ValueGetExt for Map<String, Value>

Receiver-shape widening of the READ-projection axis-family — the same three methods extended from Value (the Value::Object arm’s walker) to Map<String, Value> (the object interior itself), closing the receiver-shape gap so a caller who already holds an &Map<String, Value> handle (via .as_object().unwrap(), via ValueObjectExt::as_object_mut_or, via the two JsonMap*Ext siblings’ returns, or via a helper like ssapply::ownership_kv_pair that composes and returns a Map directly) reaches the SAME get_i64 / get_str / get_array methods without a Value::Object(m) rewrap detour.

Pre-lift the .get(<key>).and_then(Value::as_<T>) two-link chain was hand-authored at 30 Map<String, Value>-receiver sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold — 19 in tatara-reconciler::patch tests (the phase-status wire-shape pins walking obj = v.as_object().unwrap() and metadata = obj.get( "metadata").and_then(Value::as_object).unwrap() receivers) plus 11 in tatara-reconciler::ssapply tests (the ownership-tag + composed-coord pins walking the Map handles returned by ownership_annotations / ownership_labels / ownership_annotations_by_coord). Post-lift each callsite reads <map>.get_str(<key>) / <map>.get_array(<key>) and the READ chain rides through the SAME substrate owner the Value-receiver callers already threaded through.

The axis-family invariant (a caller who imports ValueGetExt reaches every axis through the same trait handle — pinned at [tests::get_array_axis_family_reaches_i64_str_and_array_through_one_trait_import] and its get_str sibling) extends verbatim to the Map receiver: a single use tatara_process::json_object::ValueGetExt; unlocks every axis on both receiver shapes. A future new axis (e.g. a get_bool for Value::Bool slots) adds one method on the trait and inherits both impls; there is no separate MapGetExt peer to keep in sync.

Theory anchor: THEORY.md §VI.1 (generation over composition — the two-link chain recurred at 30 Map-receiver sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and rides through the same substrate owner the pre-existing Value-receiver impl above already pinned). THEORY.md §II.1 invariant 5 (composition preserves proofs — the receiver-shape widening carries the axis-family invariant across without splitting it into two traits).

Source§

fn get_i64(&self, key: &str) -> Option<i64>

Source§

fn get_str(&self, key: &str) -> Option<&str>

Source§

fn get_array(&self, key: &str) -> Option<&Vec<Value>>

Source§

impl ValueGetExt for Value

Source§

fn get_i64(&self, key: &str) -> Option<i64>

Source§

fn get_str(&self, key: &str) -> Option<&str>

Source§

fn get_array(&self, key: &str) -> Option<&Vec<Value>>

Implementors§