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