Skip to main content

helm_schema_core/
contract_use.rs

1use serde::ser::SerializeStruct as _;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4use crate::{ContractProvenance, Guard, GuardDnf, ResourceRef, ValueKind, ValuesPath, YamlPath};
5
6/// The rendered text is ONE SEGMENT of the source string split by a literal
7/// separator (`regexSplit ":" . -1 | last` extracting a port suffix): the
8/// sink schema constrains that segment, never the whole raw value.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10pub struct SplitSegmentUse {
11    /// Literal delimiter used to split the source string.
12    pub separator: String,
13    /// The LAST segment when true, the first otherwise.
14    pub last: bool,
15}
16
17/// A structural transform applied to one ordered merge layer.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19pub enum MergeLayerTransform {
20    /// The layer reaches the merge unchanged.
21    Identity,
22    /// Nil members are recursively removed before the layer reaches the merge.
23    NilScrubbed,
24    /// Helm's map-only YAML decoder discards non-mapping source shapes.
25    ParsedMap,
26}
27
28/// One ordered merge input paired with the transform applied before merging.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
30pub struct MergeLayer {
31    /// Values path supplying this layer.
32    pub path: ValuesPath,
33    /// Structural transform applied before the merge reads the layer.
34    pub transform: MergeLayerTransform,
35}
36
37/// The value is one layer of an ordered Sprig `merge`: a key of an earlier
38/// layer shadows the same key of every later layer at the rendered sink, so
39/// a later layer's member reaches the sink only where every earlier layer
40/// lacks that member.
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct MergeLayersUse {
43    layers: Vec<MergeLayer>,
44    position: usize,
45    own_transform: MergeLayerTransform,
46    /// Whether the layer facts came from a local binding's metadata rather
47    /// than the render site's own layered value.
48    ///
49    /// Identity-only binding merges keep their ordinary branch routing
50    /// because sibling dispatch arms may contribute other input shapes.
51    /// Structurally transformed bindings retain layered routing so each
52    /// transform's selection semantics remain visible at emission.
53    via_binding: bool,
54}
55
56impl MergeLayersUse {
57    /// Creates a layered use when `position` selects an entry in `layers`.
58    #[must_use]
59    pub fn new(layers: Vec<MergeLayer>, position: usize, via_binding: bool) -> Option<Self> {
60        let own_transform = layers
61            .iter()
62            .enumerate()
63            .find_map(|(index, layer)| (index == position).then_some(layer.transform))?;
64        Some(Self {
65            layers,
66            position,
67            own_transform,
68            via_binding,
69        })
70    }
71
72    /// Returns every layer in precedence order.
73    #[must_use]
74    pub fn layers(&self) -> &[MergeLayer] {
75        &self.layers
76    }
77
78    /// Returns this use's checked index in [`Self::layers`].
79    #[must_use]
80    pub fn position(&self) -> usize {
81        self.position
82    }
83
84    /// Reports whether the own layer has `path`.
85    #[must_use]
86    pub fn own_path_is(&self, path: &ValuesPath) -> bool {
87        self.layers
88            .iter()
89            .enumerate()
90            .any(|(index, layer)| index == self.position && &layer.path == path)
91    }
92
93    /// Returns the higher-precedence layers whose keys shadow this layer's.
94    #[must_use]
95    pub fn shadowed_by(&self) -> impl ExactSizeIterator<Item = &MergeLayer> {
96        self.layers.iter().take(self.position)
97    }
98
99    /// Returns the transform applied to this use's layer.
100    #[must_use]
101    pub fn own_transform(&self) -> MergeLayerTransform {
102        self.own_transform
103    }
104
105    /// Reports whether any layer is structurally transformed.
106    #[must_use]
107    pub fn has_transformed_layer(&self) -> bool {
108        self.layers
109            .iter()
110            .any(|layer| layer.transform != MergeLayerTransform::Identity)
111    }
112
113    /// Reports whether these facts crossed a local binding boundary.
114    #[must_use]
115    pub fn via_binding(&self) -> bool {
116        self.via_binding
117    }
118
119    /// Marks these layer facts as crossing a local binding boundary.
120    #[must_use]
121    pub fn into_via_binding(mut self) -> Self {
122        self.via_binding = true;
123        self
124    }
125}
126
127impl Serialize for MergeLayersUse {
128    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129    where
130        S: Serializer,
131    {
132        let paths = self
133            .layers
134            .iter()
135            .map(|layer| &layer.path)
136            .collect::<Vec<_>>();
137        let transforms = self
138            .layers
139            .iter()
140            .map(|layer| layer.transform)
141            .collect::<Vec<_>>();
142        let mut state = serializer.serialize_struct("MergeLayersUse", 4)?;
143        state.serialize_field("layers", &paths)?;
144        state.serialize_field("position", &self.position)?;
145        state.serialize_field("transforms", &transforms)?;
146        state.serialize_field("via_binding", &self.via_binding)?;
147        state.end()
148    }
149}
150
151impl<'de> Deserialize<'de> for MergeLayersUse {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: Deserializer<'de>,
155    {
156        #[derive(Deserialize)]
157        struct WireMergeLayersUse {
158            layers: Vec<ValuesPath>,
159            position: usize,
160            transforms: Vec<MergeLayerTransform>,
161            via_binding: bool,
162        }
163
164        let wire = WireMergeLayersUse::deserialize(deserializer)?;
165        if wire.layers.len() != wire.transforms.len() {
166            return Err(serde::de::Error::custom(
167                "merge layer paths and transforms must have equal lengths",
168            ));
169        }
170        let layers = wire
171            .layers
172            .into_iter()
173            .zip(wire.transforms)
174            .map(|(path, transform)| MergeLayer { path, transform })
175            .collect();
176        Self::new(layers, wire.position, wire.via_binding)
177            .ok_or_else(|| serde::de::Error::custom("merge layer position is out of bounds"))
178    }
179}
180
181/// A contract claim for one observed values path.
182#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
183pub struct ContractUse {
184    /// Canonical values path or expression that supplied the rendered value.
185    pub source_expr: ValuesPath,
186    /// Structural path of the value in the rendered YAML document.
187    pub path: YamlPath,
188    /// How the value contributes to the rendered YAML node.
189    pub kind: ValueKind,
190    /// Normalized condition under which the use renders.
191    pub condition: GuardDnf,
192    /// Kubernetes resource owning the rendered path, when known.
193    pub resource: Option<ResourceRef>,
194    /// Template locations and helper chains that produced the use.
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub provenance: Vec<ContractProvenance>,
197    /// Go template execution rendered the source through its `%v` spelling,
198    /// so a provider slot observes text rather than the raw input shape.
199    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
200    pub stringified: bool,
201    /// Literal member keys the TEMPLATE writes beside this fragment splice
202    /// in the same mapping (`- name: tmp` next to `toYaml .Values.tmpVolume`):
203    /// the rendered object already has them, so a provider slot's object
204    /// requiredness must not re-demand them from the user value.
205    #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
206    pub template_supplied_member_keys: std::collections::BTreeSet<String>,
207    /// Set when the rendered text is one separator-delimited segment of the
208    /// source string rather than the raw value.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub split_segment: Option<SplitSegmentUse>,
211    /// Set when the value renders as one layer of an ordered `merge`.
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub merge_layers: Option<MergeLayersUse>,
214    /// Set when the rendered text is the collection's RANGE KEY rather than
215    /// its value: the sink constrains the key domain only.
216    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
217    pub range_key: bool,
218    /// The rendered text is a Sprig `quote`/`squote` of the value, which
219    /// skips nil operands: a missing or null source renders an explicit
220    /// YAML null into the sink (see
221    /// [`ProviderSchemaUse::nil_omitting`](crate::ProviderSchemaUse::nil_omitting)).
222    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
223    pub nil_omitting: bool,
224    /// Literal member keys a guard-scoped `omit` may remove from the
225    /// rendered map before the sink reads it. Each key maps to the sound
226    /// RETAIN guards under which the key certainly survives (the omitting
227    /// arm certainly did not run); an empty guard list means the key's
228    /// survival is undecidable and its sink typing must abstain entirely.
229    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
230    pub omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
231    /// Set when the slot renders fresh text DERIVED from the value
232    /// (`include … | sha256sum` checksum annotations): the sink observes
233    /// neither the value nor its serialization, so the row grants its
234    /// branch serialized tolerance without claiming a path-wide
235    /// serialization use.
236    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
237    pub digest: bool,
238    /// Set when the value flowed through a Sprig `merge` call as a DIRECT
239    /// operand: the operand's strict map contract rides its own fail
240    /// implication (keyed on the call's live gate), so this row never
241    /// rejects a Helm-falsy input at the base.
242    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
243    pub merge_operand: bool,
244}
245
246impl ContractUse {
247    /// Creates a contract use from one conjunction of guards.
248    #[must_use]
249    pub fn new(
250        source_expr: ValuesPath,
251        path: YamlPath,
252        kind: ValueKind,
253        guards: Vec<Guard>,
254        resource: Option<ResourceRef>,
255    ) -> Self {
256        Self::with_provenances(source_expr, path, kind, guards, resource, None)
257    }
258
259    /// Creates a guarded contract use with explicit source provenance.
260    pub fn with_provenances(
261        source_expr: ValuesPath,
262        path: YamlPath,
263        kind: ValueKind,
264        guards: Vec<Guard>,
265        resource: Option<ResourceRef>,
266        provenance: impl IntoIterator<Item = ContractProvenance>,
267    ) -> Self {
268        let condition = GuardDnf::from_guards(guards);
269        Self::with_condition_and_provenances(
270            source_expr,
271            path,
272            kind,
273            condition,
274            resource,
275            provenance,
276        )
277    }
278
279    /// Creates a contract use from an already-normalized condition.
280    pub fn with_condition_and_provenances(
281        source_expr: ValuesPath,
282        path: YamlPath,
283        kind: ValueKind,
284        condition: GuardDnf,
285        resource: Option<ResourceRef>,
286        provenance: impl IntoIterator<Item = ContractProvenance>,
287    ) -> Self {
288        Self {
289            source_expr,
290            path,
291            kind,
292            condition,
293            resource,
294            provenance: provenance.into_iter().collect(),
295            stringified: false,
296            template_supplied_member_keys: std::collections::BTreeSet::new(),
297            split_segment: None,
298            merge_layers: None,
299            range_key: false,
300            nil_omitting: false,
301            omitted_members: std::collections::BTreeMap::new(),
302            digest: false,
303            merge_operand: false,
304        }
305    }
306
307    /// Sorts and deduplicates provenance without changing semantic evidence.
308    pub fn canonicalize(&mut self) {
309        self.provenance.sort();
310        self.provenance.dedup();
311    }
312
313    /// Returns the sole guard conjunction, or an empty conjunction when not singular.
314    #[must_use]
315    pub fn single_guard_conjunction(&self) -> Vec<Guard> {
316        self.condition
317            .single_guard_conjunction()
318            .unwrap_or_default()
319    }
320
321    /// Rewrites the source expression and every values path in the condition.
322    pub fn map_value_paths<F>(&mut self, map: &mut F)
323    where
324        F: FnMut(ValuesPath) -> ValuesPath,
325    {
326        let Self {
327            source_expr,
328            path: _,
329            kind: _,
330            condition,
331            resource: _,
332            provenance: _,
333            stringified: _,
334            template_supplied_member_keys: _,
335            split_segment: _,
336            merge_layers,
337            range_key: _,
338            nil_omitting: _,
339            omitted_members,
340            digest: _,
341            merge_operand: _,
342        } = self;
343        *source_expr = map(source_expr.clone());
344        condition.map_value_paths(map);
345        if let Some(merge) = merge_layers {
346            for layer in &mut merge.layers {
347                layer.path = map(layer.path.clone());
348            }
349        }
350        for retain_guards in omitted_members.values_mut() {
351            for guard in retain_guards {
352                *guard = guard.clone().map_value_paths(map);
353            }
354        }
355    }
356}