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/// 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}