1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
use serde::{Deserialize, Serialize};
use crate::{ContractProvenance, Guard, GuardDnf, ResourceRef, ValueKind, YamlPath};
/// The rendered text is ONE SEGMENT of the source string split by a literal
/// separator (`regexSplit ":" . -1 | last` extracting a port suffix): the
/// sink schema constrains that segment, never the whole raw value.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SplitSegmentUse {
/// Literal delimiter used to split the source string.
pub separator: String,
/// The LAST segment when true, the first otherwise.
pub last: bool,
}
/// The value is one layer of an ordered Sprig `merge`: a key of an earlier
/// layer shadows the same key of every later layer at the rendered sink, so
/// a later layer's member reaches the sink only where every earlier layer
/// lacks that member.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MergeLayersUse {
/// Every layer's values path, highest precedence first.
pub layers: Vec<String>,
/// This use's own index within `layers`.
pub position: usize,
/// Per-layer scrub markers, parallel to `layers`: a `true` layer's map
/// had nil members recursively removed before the merge (airflow's
/// `removeNilFields`), so that layer's sink typing must admit null
/// member spellings, and binding-carried rows of a scrub-involving
/// merge keep the layered routing.
pub nil_scrubbed_layers: Vec<bool>,
/// The layer facts were recovered from a local BINDING's meta (the
/// merged value flowed through a helper output before rendering)
/// rather than the render site's own layered value. Such rows keep
/// their base metadata field kind: the binding's other dispatch arms
/// (bitnami's `tplvalues.render` string lane) rely on the string-map
/// alternative it contributes, while direct render-site layers moved
/// that typing onto the synthesized arms entirely.
pub via_binding: bool,
}
impl MergeLayersUse {
/// The higher-precedence layer paths whose keys shadow this layer's.
#[must_use]
pub fn shadowed_by(&self) -> &[String] {
self.layers
.get(..self.position.min(self.layers.len()))
.unwrap_or_default()
}
}
/// A contract claim for one observed values path.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct ContractUse {
/// Canonical values path or expression that supplied the rendered value.
pub source_expr: String,
/// Structural path of the value in the rendered YAML document.
pub path: YamlPath,
/// How the value contributes to the rendered YAML node.
pub kind: ValueKind,
/// Normalized condition under which the use renders.
pub condition: GuardDnf,
/// Kubernetes resource owning the rendered path, when known.
pub resource: Option<ResourceRef>,
/// Template locations and helper chains that produced the use.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provenance: Vec<ContractProvenance>,
/// A string-consuming transform (`trunc`, `b64enc`, a dynamic `printf`
/// format) produced this rendered text: rendering fails for non-string
/// values, but only where THIS row's condition holds.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub has_string_contract: bool,
/// Literal member keys the TEMPLATE writes beside this fragment splice
/// in the same mapping (`- name: tmp` next to `toYaml .Values.tmpVolume`):
/// the rendered object already has them, so a provider slot's object
/// requiredness must not re-demand them from the user value.
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
pub template_supplied_member_keys: std::collections::BTreeSet<String>,
/// Set when the rendered text is one separator-delimited segment of the
/// source string rather than the raw value.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub split_segment: Option<SplitSegmentUse>,
/// Set when the value renders as one layer of an ordered `merge`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merge_layers: Option<MergeLayersUse>,
/// Set when the rendered text is the collection's RANGE KEY rather than
/// its value: the sink constrains the key domain only.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub range_key: bool,
/// The rendered text is a Sprig `quote`/`squote` of the value, which
/// skips nil operands: a missing or null source renders an explicit
/// YAML null into the sink (see
/// [`ProviderSchemaUse::nil_omitting`](crate::ProviderSchemaUse::nil_omitting)).
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub nil_omitting: bool,
/// Literal member keys a guard-scoped `omit` may remove from the
/// rendered map before the sink reads it. Each key maps to the sound
/// RETAIN guards under which the key certainly survives (the omitting
/// arm certainly did not run); an empty guard list means the key's
/// survival is undecidable and its sink typing must abstain entirely.
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
/// Set when the slot renders fresh text DERIVED from the value
/// (`include … | sha256sum` checksum annotations): the sink observes
/// neither the value nor its serialization, so the row grants its
/// branch serialized tolerance without claiming a path-wide
/// serialization use.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub digest: bool,
/// Set when the value flowed through a Sprig `merge` call as a DIRECT
/// operand: the operand's strict map contract rides its own fail
/// implication (keyed on the call's live gate), so this row never
/// rejects a Helm-falsy input at the base.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub merge_operand: bool,
}
impl<'de> Deserialize<'de> for ContractUse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct WireContractUse {
source_expr: String,
path: YamlPath,
kind: ValueKind,
condition: GuardDnf,
resource: Option<ResourceRef>,
#[serde(default)]
provenance: Vec<ContractProvenance>,
#[serde(default)]
has_string_contract: bool,
#[serde(default)]
template_supplied_member_keys: std::collections::BTreeSet<String>,
#[serde(default)]
split_segment: Option<SplitSegmentUse>,
#[serde(default)]
merge_layers: Option<MergeLayersUse>,
#[serde(default)]
range_key: bool,
#[serde(default)]
nil_omitting: bool,
#[serde(default)]
omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
#[serde(default)]
digest: bool,
#[serde(default)]
merge_operand: bool,
}
let wire = WireContractUse::deserialize(deserializer)?;
Ok(Self {
source_expr: wire.source_expr,
path: wire.path,
kind: wire.kind,
condition: wire.condition,
resource: wire.resource,
provenance: wire.provenance,
has_string_contract: wire.has_string_contract,
template_supplied_member_keys: wire.template_supplied_member_keys,
split_segment: wire.split_segment,
merge_layers: wire.merge_layers,
range_key: wire.range_key,
nil_omitting: wire.nil_omitting,
omitted_members: wire.omitted_members,
digest: wire.digest,
merge_operand: wire.merge_operand,
})
}
}
impl ContractUse {
/// Creates a contract use from one conjunction of guards.
#[must_use]
pub fn new(
source_expr: String,
path: YamlPath,
kind: ValueKind,
guards: Vec<Guard>,
resource: Option<ResourceRef>,
) -> Self {
Self::with_provenances(source_expr, path, kind, guards, resource, None)
}
/// Creates a guarded contract use with explicit source provenance.
pub fn with_provenances(
source_expr: String,
path: YamlPath,
kind: ValueKind,
guards: Vec<Guard>,
resource: Option<ResourceRef>,
provenance: impl IntoIterator<Item = ContractProvenance>,
) -> Self {
let condition = GuardDnf::from_guards(guards);
Self::with_condition_and_provenances(
source_expr,
path,
kind,
condition,
resource,
provenance,
)
}
/// Creates a contract use from an already-normalized condition.
pub fn with_condition_and_provenances(
source_expr: String,
path: YamlPath,
kind: ValueKind,
condition: GuardDnf,
resource: Option<ResourceRef>,
provenance: impl IntoIterator<Item = ContractProvenance>,
) -> Self {
Self {
source_expr,
path,
kind,
condition,
resource,
provenance: provenance.into_iter().collect(),
has_string_contract: false,
template_supplied_member_keys: std::collections::BTreeSet::new(),
split_segment: None,
merge_layers: None,
range_key: false,
nil_omitting: false,
omitted_members: std::collections::BTreeMap::new(),
digest: false,
merge_operand: false,
}
}
/// Sorts and deduplicates provenance without changing semantic evidence.
pub fn canonicalize(&mut self) {
self.provenance.sort();
self.provenance.dedup();
}
/// Returns the sole guard conjunction, or an empty conjunction when not singular.
#[must_use]
pub fn single_guard_conjunction(&self) -> Vec<Guard> {
self.condition
.single_guard_conjunction()
.unwrap_or_default()
}
/// Rewrites the source expression and every values path in the condition.
pub fn map_value_paths<F>(&mut self, map: &mut F)
where
F: FnMut(&str) -> String,
{
self.source_expr = map(&self.source_expr);
self.condition.map_value_paths(map);
}
}