Skip to main content

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/// A structural transform applied to one ordered merge layer.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18pub enum MergeLayerTransform {
19    /// The layer reaches the merge unchanged.
20    Identity,
21    /// Nil members are recursively removed before the layer reaches the merge.
22    NilScrubbed,
23    /// Helm's map-only YAML decoder discards non-mapping source shapes.
24    ParsedMap,
25}
26
27/// The value is one layer of an ordered Sprig `merge`: a key of an earlier
28/// layer shadows the same key of every later layer at the rendered sink, so
29/// a later layer's member reaches the sink only where every earlier layer
30/// lacks that member.
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32pub struct MergeLayersUse {
33    /// Every layer's values path, highest precedence first.
34    pub layers: Vec<String>,
35    /// This use's own index within `layers`.
36    pub position: usize,
37    /// Per-layer transforms, parallel to `layers`.
38    pub transforms: Vec<MergeLayerTransform>,
39    /// Whether the layer facts came from a local binding's metadata rather
40    /// than the render site's own layered value.
41    ///
42    /// Identity-only binding merges keep their ordinary branch routing
43    /// because sibling dispatch arms may contribute other input shapes.
44    /// Structurally transformed bindings retain layered routing so each
45    /// transform's selection semantics remain visible at emission.
46    pub via_binding: bool,
47}
48
49impl MergeLayersUse {
50    /// The higher-precedence layer paths whose keys shadow this layer's.
51    #[must_use]
52    pub fn shadowed_by(&self) -> &[String] {
53        self.layers
54            .get(..self.position.min(self.layers.len()))
55            .unwrap_or_default()
56    }
57
58    /// Returns the transform applied to this use's layer.
59    #[must_use]
60    pub fn own_transform(&self) -> MergeLayerTransform {
61        self.transforms
62            .get(self.position)
63            .copied()
64            .unwrap_or(MergeLayerTransform::Identity)
65    }
66}
67
68/// A contract claim for one observed values path.
69#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
70pub struct ContractUse {
71    /// Canonical values path or expression that supplied the rendered value.
72    pub source_expr: String,
73    /// Structural path of the value in the rendered YAML document.
74    pub path: YamlPath,
75    /// How the value contributes to the rendered YAML node.
76    pub kind: ValueKind,
77    /// Normalized condition under which the use renders.
78    pub condition: GuardDnf,
79    /// Kubernetes resource owning the rendered path, when known.
80    pub resource: Option<ResourceRef>,
81    /// Template locations and helper chains that produced the use.
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub provenance: Vec<ContractProvenance>,
84    /// Go template execution rendered the source through its `%v` spelling,
85    /// so a provider slot observes text rather than the raw input shape.
86    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
87    pub stringified: bool,
88    /// Literal member keys the TEMPLATE writes beside this fragment splice
89    /// in the same mapping (`- name: tmp` next to `toYaml .Values.tmpVolume`):
90    /// the rendered object already has them, so a provider slot's object
91    /// requiredness must not re-demand them from the user value.
92    #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
93    pub template_supplied_member_keys: std::collections::BTreeSet<String>,
94    /// Set when the rendered text is one separator-delimited segment of the
95    /// source string rather than the raw value.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub split_segment: Option<SplitSegmentUse>,
98    /// Set when the value renders as one layer of an ordered `merge`.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub merge_layers: Option<MergeLayersUse>,
101    /// Set when the rendered text is the collection's RANGE KEY rather than
102    /// its value: the sink constrains the key domain only.
103    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
104    pub range_key: bool,
105    /// The rendered text is a Sprig `quote`/`squote` of the value, which
106    /// skips nil operands: a missing or null source renders an explicit
107    /// YAML null into the sink (see
108    /// [`ProviderSchemaUse::nil_omitting`](crate::ProviderSchemaUse::nil_omitting)).
109    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
110    pub nil_omitting: bool,
111    /// Literal member keys a guard-scoped `omit` may remove from the
112    /// rendered map before the sink reads it. Each key maps to the sound
113    /// RETAIN guards under which the key certainly survives (the omitting
114    /// arm certainly did not run); an empty guard list means the key's
115    /// survival is undecidable and its sink typing must abstain entirely.
116    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
117    pub omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
118    /// Set when the slot renders fresh text DERIVED from the value
119    /// (`include … | sha256sum` checksum annotations): the sink observes
120    /// neither the value nor its serialization, so the row grants its
121    /// branch serialized tolerance without claiming a path-wide
122    /// serialization use.
123    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
124    pub digest: bool,
125    /// Set when the value flowed through a Sprig `merge` call as a DIRECT
126    /// operand: the operand's strict map contract rides its own fail
127    /// implication (keyed on the call's live gate), so this row never
128    /// rejects a Helm-falsy input at the base.
129    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
130    pub merge_operand: bool,
131}
132
133impl<'de> Deserialize<'de> for ContractUse {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: serde::Deserializer<'de>,
137    {
138        #[derive(Deserialize)]
139        struct WireContractUse {
140            source_expr: String,
141            path: YamlPath,
142            kind: ValueKind,
143            condition: GuardDnf,
144            resource: Option<ResourceRef>,
145            #[serde(default)]
146            provenance: Vec<ContractProvenance>,
147            #[serde(default)]
148            stringified: bool,
149            #[serde(default)]
150            template_supplied_member_keys: std::collections::BTreeSet<String>,
151            #[serde(default)]
152            split_segment: Option<SplitSegmentUse>,
153            #[serde(default)]
154            merge_layers: Option<MergeLayersUse>,
155            #[serde(default)]
156            range_key: bool,
157            #[serde(default)]
158            nil_omitting: bool,
159            #[serde(default)]
160            omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
161            #[serde(default)]
162            digest: bool,
163            #[serde(default)]
164            merge_operand: bool,
165        }
166
167        let wire = WireContractUse::deserialize(deserializer)?;
168        Ok(Self {
169            source_expr: wire.source_expr,
170            path: wire.path,
171            kind: wire.kind,
172            condition: wire.condition,
173            resource: wire.resource,
174            provenance: wire.provenance,
175            stringified: wire.stringified,
176            template_supplied_member_keys: wire.template_supplied_member_keys,
177            split_segment: wire.split_segment,
178            merge_layers: wire.merge_layers,
179            range_key: wire.range_key,
180            nil_omitting: wire.nil_omitting,
181            omitted_members: wire.omitted_members,
182            digest: wire.digest,
183            merge_operand: wire.merge_operand,
184        })
185    }
186}
187
188impl ContractUse {
189    /// Creates a contract use from one conjunction of guards.
190    #[must_use]
191    pub fn new(
192        source_expr: String,
193        path: YamlPath,
194        kind: ValueKind,
195        guards: Vec<Guard>,
196        resource: Option<ResourceRef>,
197    ) -> Self {
198        Self::with_provenances(source_expr, path, kind, guards, resource, None)
199    }
200
201    /// Creates a guarded contract use with explicit source provenance.
202    pub fn with_provenances(
203        source_expr: String,
204        path: YamlPath,
205        kind: ValueKind,
206        guards: Vec<Guard>,
207        resource: Option<ResourceRef>,
208        provenance: impl IntoIterator<Item = ContractProvenance>,
209    ) -> Self {
210        let condition = GuardDnf::from_guards(guards);
211        Self::with_condition_and_provenances(
212            source_expr,
213            path,
214            kind,
215            condition,
216            resource,
217            provenance,
218        )
219    }
220
221    /// Creates a contract use from an already-normalized condition.
222    pub fn with_condition_and_provenances(
223        source_expr: String,
224        path: YamlPath,
225        kind: ValueKind,
226        condition: GuardDnf,
227        resource: Option<ResourceRef>,
228        provenance: impl IntoIterator<Item = ContractProvenance>,
229    ) -> Self {
230        Self {
231            source_expr,
232            path,
233            kind,
234            condition,
235            resource,
236            provenance: provenance.into_iter().collect(),
237            stringified: false,
238            template_supplied_member_keys: std::collections::BTreeSet::new(),
239            split_segment: None,
240            merge_layers: None,
241            range_key: false,
242            nil_omitting: false,
243            omitted_members: std::collections::BTreeMap::new(),
244            digest: false,
245            merge_operand: false,
246        }
247    }
248
249    /// Sorts and deduplicates provenance without changing semantic evidence.
250    pub fn canonicalize(&mut self) {
251        self.provenance.sort();
252        self.provenance.dedup();
253    }
254
255    /// Returns the sole guard conjunction, or an empty conjunction when not singular.
256    #[must_use]
257    pub fn single_guard_conjunction(&self) -> Vec<Guard> {
258        self.condition
259            .single_guard_conjunction()
260            .unwrap_or_default()
261    }
262
263    /// Rewrites the source expression and every values path in the condition.
264    pub fn map_value_paths<F>(&mut self, map: &mut F)
265    where
266        F: FnMut(&str) -> String,
267    {
268        self.source_expr = map(&self.source_expr);
269        self.condition.map_value_paths(map);
270        if let Some(merge) = &mut self.merge_layers {
271            for layer in &mut merge.layers {
272                *layer = map(layer);
273            }
274        }
275        for retain_guards in self.omitted_members.values_mut() {
276            for guard in retain_guards {
277                *guard = guard.clone().map_value_paths(map);
278            }
279        }
280    }
281}