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>;
}
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). Adding a new axis (a get_bool for Value::Bool, a get_object for Value::Object, a get_array for Value::Array) 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).

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 Value

Source§

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

Source§

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

Implementors§