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 everyJobAttested+ClosedLoopAuthpostcondition evaluator gates on (succeeded < 1short-circuits toSatisfaction::Unsatisfied("… still running (…)")).status.get("failed").and_then(|v| v.as_i64())— the Job-failure counter the same evaluators gate on (failed > 0short-circuits toSatisfaction::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 inherentserde_json::Value::get’s primarystr-index arm. A caller with a runtime-computed key (aStringproduced by a template composer) reaches through.get_i64(&s)mechanically viaDeref<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, aValue::Null) returnNoneverbatim via the inherentValue::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§
Sourcefn get_i64(&self, key: &str) -> Option<i64>
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.
Sourcefn get_str(&self, key: &str) -> Option<&str>
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 aserde_json::Valuerendered manifest into the typedRenderedResourceCoordsrow; the required three (apiVersion/kind/metadata.name) compose with.ok_or_else(|| anyhow!("rendered resource missing X"))? .to_string(), and the optionalmetadata.namespacecomposes 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 aValue::Stringslot off a K8s Condition object off thestatus.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".