tatara_process/string_map.rs
1//! Substrate primitive over `std::collections::BTreeMap<String, String>`
2//! — the ONE substrate owner of the `<map>.insert(<k>.to_string(),
3//! <v>.to_string())` string-string insertion shape every K8s-carrier
4//! writer (ObjectMeta `annotations` / `labels`, ConfigMap `data`)
5//! restates by hand at the `&str × &str → BTreeMap` write boundary.
6//!
7//! Receiver-shape peer of [`crate::json_object::JsonMapStrExt::insert_str`]
8//! on the "insert a string at a string key" write axis, partitioned by
9//! CARRIER TYPE:
10//!
11//! * [`crate::json_object::JsonMapStrExt::insert_str`] — the
12//! `serde_json::Map<String, Value>` receiver used by every JSON-shaped
13//! `metadata.annotations` / label map / spec-body slot the reconciler
14//! emits to K8s through SSA-time `serde_json::Value` bodies.
15//! * [`BTreeMapStrExt::insert_str`] (this trait) — the
16//! `BTreeMap<String, String>` receiver used by every K8s-canonical
17//! ObjectMeta annotations / labels slot AND every ConfigMap `.data`
18//! slot the workspace stamps through the `kube-rs` typed API surface
19//! (which reifies `ObjectMeta.annotations: Option<BTreeMap<String,
20//! String>>` verbatim).
21//!
22//! The two traits deliberately share the `insert_str` method name AND
23//! the `(impl Into<String>, impl Into<String>)` argument shape so a
24//! caller who imports either substrate reaches the same-shape write
25//! call at the same-name method, regardless of whether the receiver is
26//! the JSON-side `Map<String, Value>` or the K8s-typed-API-side
27//! `BTreeMap<String, String>`. A future new carrier (e.g. a
28//! `HashMap<String, String>` receiver for a lightweight fixture map,
29//! or a `secrecy::Secret<String>` value-slot for encrypted secret
30//! payloads) adds one impl arm here without splitting the substrate
31//! into a third trait.
32//!
33//! Pre-lift the shape was hand-authored at THREE production sites
34//! across two workspace crates past the ★★ PRIME-DIRECTIVE ≥ 2
35//! duplication threshold:
36//!
37//! * `tatara-pool-reconciler::controller_pool::build_member_process`
38//! × 2 — the pool-membership annotation seed stamping
39//! `annotations::POOL` + `annotations::POOL_SLOT` into the fresh
40//! member Process's `metadata.annotations` map right after `Process::
41//! new`. Both restated the same `<map>.insert(<key>.to_string(),
42//! <val>.to_string())` shape.
43//! * `tatara-export-worker::write_receipt` × 1 — the receipt-CM `.data`
44//! seed stamping the `(configmap-key, payload)` pair into the fresh
45//! `BTreeMap` right before the [`crate::configmap::with_data`]
46//! composer wraps it as a `ConfigMap` wire body.
47//!
48//! All three sites walked the SAME two-position insert shape verbatim,
49//! differing only in the `&str` / `&'static str` key + the `&str` /
50//! numeric-`.to_string()` value at each callsite. Post-lift each
51//! callsite reads `<map>.insert_str(<key>, <val>)` and the string-
52//! string write shape lives at ONE substrate owner here.
53//!
54//! ### Naming — `insert_str`, not `insert`
55//!
56//! Same discipline as the sibling
57//! [`crate::json_object::JsonMapStrExt::insert_str`] — the trait method
58//! deliberately does NOT collide with the inherent `BTreeMap::insert`
59//! (which takes `(String, String)` positionally). A name collision
60//! would let a caller who has [`BTreeMapStrExt`] in scope resolve to
61//! the inherent method by accident (inherent methods win over trait
62//! methods in method resolution) and silently drop the `Into<String>`
63//! coerce on either slot. The `_str` suffix names the intent: both
64//! slots project the caller's borrowed handle into an owned `String`
65//! at the substrate, not at every callsite.
66//!
67//! Theory anchor: THEORY.md §VI.1 (generation over composition — the
68//! `<map>.insert(<k>.to_string(), <v>.to_string())` shape recurred at
69//! three hand-authored production sites across two workspace crates
70//! past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted
71//! to ONE substrate owner here). THEORY.md §II.1 invariant 5
72//! (composition preserves proofs — a regression that drifts the write
73//! shape at ONE consumer surfaces at the substrate pin rather than as
74//! silent per-emit skew across every K8s-carrier annotations / labels
75//! / ConfigMap-data writer).
76
77use std::collections::BTreeMap;
78
79/// Substrate extension trait over `BTreeMap<String, String>` — the ONE
80/// substrate owner of the `<map>.insert(<k>.to_string(), <v>.to_string
81/// ())` string-string insertion shape every K8s-carrier writer
82/// (ObjectMeta annotations / labels, ConfigMap `.data`) hand-authored
83/// at the callsite pre-lift.
84///
85/// Peer of [`crate::json_object::JsonMapStrExt`] on the same
86/// "insert a string at a string key" write axis, split by receiver
87/// type (JSON-shaped `Map<String, Value>` vs K8s-canonical
88/// `BTreeMap<String, String>`). See the module docs for the naming
89/// rationale (why `insert_str` and not `insert`) and the callsite
90/// audit.
91pub trait BTreeMapStrExt {
92 /// Insert an owned `String` value at an owned `String` key into
93 /// this K8s-canonical string-string map. Returns `Option<String>`
94 /// matching the underlying [`BTreeMap::insert`] semantics — `None`
95 /// for a new key, `Some(prev)` for an overwrite of an existing
96 /// slot.
97 ///
98 /// Both slots accept any `impl Into<String>` — `&str` (via
99 /// `String::from`), `String` (identity), `Cow<'_, str>`, so a
100 /// callsite with a static `annotations::POOL` (`&'static str`)
101 /// reads `insert_str(annotations::POOL, pool_name)` with no
102 /// `.to_string()` per-site. Numeric or non-string values still
103 /// need an explicit `.to_string()` at the callsite — same as
104 /// pre-lift, so the wrapping shape stays visible in the caller's
105 /// grep footprint.
106 fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<String>;
107}
108
109impl BTreeMapStrExt for BTreeMap<String, String> {
110 #[inline]
111 fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<String> {
112 self.insert(key.into(), value.into())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 // ─── BTreeMapStrExt::insert_str substrate pins ──────────────────
121 //
122 // Fail-before-pass-after granularity: the `BTreeMapStrExt::insert_str`
123 // trait method did not exist before this commit, so each test below
124 // fails to compile pre-lift. Post-lift they collectively pin the
125 // string-string insert shape at ONE substrate owner — a regression
126 // that widened the return shape (e.g. `Result<Option<String>>`),
127 // dropped the `Into<String>` coerce on either slot, or promoted a
128 // silent-write arm that swallows overwrites surfaces HERE rather
129 // than as silent operator-facing skew across the three pre-lift
130 // consumer callsites whose observable K8s annotations / labels /
131 // ConfigMap-data payload already encoded the flat `String → String`
132 // write shape.
133
134 #[test]
135 fn insert_str_new_key_returns_none_and_writes_the_slot() {
136 // New-key invariant: an insert onto a fresh map returns `None`
137 // (byte-identical to the inherent `BTreeMap::insert` semantic
138 // the pre-lift chain rode). Post-lift the composed return
139 // matches the pre-lift chain bytewise so every downstream
140 // consumer that binds the return (a caller sensing overwrite
141 // vs new-key via `if let Some(prev) = ...` or `.is_some()`
142 // gate) inherits the pre-lift semantics verbatim.
143 let mut m: BTreeMap<String, String> = BTreeMap::new();
144 let prev = m.insert_str("tatara.pleme.io/pool", "primary");
145 assert_eq!(prev, None, "new-key insert must return None");
146 assert_eq!(
147 m.get("tatara.pleme.io/pool").map(String::as_str),
148 Some("primary")
149 );
150 }
151
152 #[test]
153 fn insert_str_existing_key_returns_previous_and_overwrites() {
154 // Overwrite invariant: an insert onto an already-populated
155 // slot returns the previous `String` value (byte-identical to
156 // the inherent `BTreeMap::insert` semantic). A regression that
157 // dropped the overwrite return (returning `None` even on
158 // pre-populated slots) would silently pass every new-key pin
159 // above and surface HERE.
160 let mut m: BTreeMap<String, String> = BTreeMap::new();
161 m.insert("tatara.pleme.io/pool".to_string(), "old".to_string());
162 let prev = m.insert_str("tatara.pleme.io/pool", "new");
163 assert_eq!(
164 prev,
165 Some("old".to_string()),
166 "overwrite must return the prior value"
167 );
168 assert_eq!(
169 m.get("tatara.pleme.io/pool").map(String::as_str),
170 Some("new")
171 );
172 }
173
174 #[test]
175 fn insert_str_matches_pre_lift_chain_bytewise_across_both_slot_shapes() {
176 // Byte-identical parity witness — the substrate composer's
177 // returned `BTreeMap` state MUST match the pre-lift `.insert
178 // (<k>.to_string(), <v>.to_string())` chain's returned state
179 // bytewise on every slot shape a pre-lift caller threaded.
180 // Sweeps the two production shapes:
181 //
182 // 1. `&'static str` key + `&str` value — the
183 // `annotations::POOL` + `pool_name` shape at
184 // `tatara-pool-reconciler::controller_pool::
185 // build_member_process`.
186 // 2. `&'static str` key + `String` value (from numeric
187 // `.to_string()`) — the `annotations::POOL_SLOT` + `slot
188 // .to_string()` shape at the same production site (the
189 // `slot: u32` argument coerces via `.to_string()` at the
190 // callsite before reaching the primitive).
191 //
192 // A regression that drifted either slot's `Into<String>` arm
193 // (a caller who reached for `.insert(k, v)` after refactoring
194 // from a `String` value slot to a plain `&str` value slot)
195 // would type-mismatch at the substrate rather than silently
196 // slip through with byte-different `String` content.
197 let key: &'static str = "tatara.pleme.io/pool";
198 let value_borrowed: &str = "primary";
199 let value_owned: String = 7u32.to_string();
200
201 let mut via_composer: BTreeMap<String, String> = BTreeMap::new();
202 via_composer.insert_str(key, value_borrowed);
203 via_composer.insert_str("tatara.pleme.io/pool-slot", value_owned.clone());
204
205 let mut via_pre_lift: BTreeMap<String, String> = BTreeMap::new();
206 via_pre_lift.insert(key.to_string(), value_borrowed.to_string());
207 via_pre_lift.insert("tatara.pleme.io/pool-slot".to_string(), value_owned);
208
209 assert_eq!(
210 via_composer, via_pre_lift,
211 "BTreeMapStrExt::insert_str must be byte-identical to the pre-lift .insert(<k>.to_string(), <v>.to_string()) chain",
212 );
213 }
214
215 #[test]
216 fn insert_str_accepts_owned_string_on_both_slots() {
217 // The `impl Into<String>` bound on both slots must accept an
218 // owned `String` (identity `Into` impl) verbatim — a fixture
219 // caller that composes both slots dynamically (as
220 // `String::from_utf8_lossy` output, format!-produced payloads,
221 // etc.) reaches the same primitive without a per-callsite
222 // borrow detour. A regression that narrowed either bound to
223 // `&str` only (via an accidental `impl AsRef<str>` swap) would
224 // reject the owned-`String` corner and fail here.
225 let k: String = "tatara.pleme.io/pool".to_string();
226 let v: String = "primary".to_string();
227 let mut m: BTreeMap<String, String> = BTreeMap::new();
228 let prev = m.insert_str(k, v);
229 assert_eq!(prev, None);
230 assert_eq!(
231 m.get("tatara.pleme.io/pool").map(String::as_str),
232 Some("primary")
233 );
234 }
235}