Skip to main content

helm_schema_ir/contract/
finalized.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use super::{ContractDocument, ContractUse};
4use crate::contract_signal_builder::derive_schema_signals_from_contract_parts;
5use crate::observed_facts::ObservedFacts;
6use helm_schema_core::{ConditionalGuard, ContractSchemaSignals, GuardedValuesDefaultSource};
7
8fn activation_guards(guards: &[crate::Guard]) -> Option<Vec<ConditionalGuard>> {
9    let mut lowered = guards
10        .iter()
11        .map(ConditionalGuard::try_from)
12        .collect::<Result<Vec<_>, _>>()
13        .ok()?;
14    lowered.sort();
15    lowered.dedup();
16    Some(lowered)
17}
18
19fn activation_guard_disjunction(guard_sets: Vec<Vec<ConditionalGuard>>) -> Vec<ConditionalGuard> {
20    let mut guard_sets =
21        helm_schema_core::GuardDnf::normalize_conditional_guard_disjunction(guard_sets);
22    match guard_sets.as_mut_slice() {
23        [] => Vec::new(),
24        [guards] => std::mem::take(guards),
25        _ => vec![ConditionalGuard::AnyOf(
26            guard_sets
27                .into_iter()
28                .map(|mut guards| match guards.len() {
29                    1 => guards.remove(0),
30                    _ => ConditionalGuard::AllOf(guards),
31                })
32                .collect(),
33        )],
34    }
35}
36
37/// Finalized contract artifact derived from one canonical normalized contract.
38///
39/// Stable inspection rows and schema-lowering signals come from the same
40/// normalized contract uses, so downstream callers do not need to re-finalize
41/// a [`super::ContractIr`] separately or hop through another wrapper type.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct FinalizedContract {
44    uses: Vec<ContractUse>,
45    schema_signals: ContractSchemaSignals,
46}
47
48impl FinalizedContract {
49    pub(in crate::contract) fn new(
50        normalized_uses: Vec<ContractUse>,
51        observed_facts: &ObservedFacts,
52        values_program_wrappers: BTreeSet<helm_schema_core::ValuesProgramWrapper>,
53        values_program_wrapper_exclusions: BTreeSet<helm_schema_core::ValuesPath>,
54        dependency_values_root_fragments: &BTreeSet<String>,
55    ) -> Self {
56        let mut default_guard_sets = BTreeMap::<_, Vec<Vec<ConditionalGuard>>>::new();
57        for fact in &observed_facts.activated_values_default_sources {
58            if let Some(guards) = activation_guards(&fact.guards) {
59                default_guard_sets
60                    .entry(fact.source.clone())
61                    .or_default()
62                    .push(guards);
63            }
64        }
65        let guarded_values_default_sources = default_guard_sets
66            .into_iter()
67            .map(|(source, guard_sets)| GuardedValuesDefaultSource {
68                outer_guards: activation_guard_disjunction(guard_sets),
69                source,
70            })
71            .collect::<Vec<_>>();
72        let mut overlay_guard_sets = BTreeMap::<_, Vec<Vec<ConditionalGuard>>>::new();
73        for fact in &observed_facts.activated_values_root_overlays {
74            if let Some(guards) = activation_guards(&fact.guards) {
75                overlay_guard_sets
76                    .entry((fact.target_path.clone(), fact.source_path.clone()))
77                    .or_default()
78                    .push(guards);
79            }
80        }
81        let guarded_root_overlays = overlay_guard_sets
82            .into_iter()
83            .map(|((target_path, source_path), guard_sets)| {
84                (
85                    activation_guard_disjunction(guard_sets),
86                    target_path,
87                    source_path,
88                )
89            })
90            .chain(observed_facts.values_root_overlays.iter().map(|fact| {
91                (
92                    Vec::new(),
93                    fact.target_path.clone(),
94                    fact.source_path.clone(),
95                )
96            }))
97            .collect::<Vec<_>>();
98        let schema_signals = derive_schema_signals_from_contract_parts(
99            &normalized_uses,
100            observed_facts,
101            dependency_values_root_fragments,
102        )
103        .with_values_default_sources(observed_facts.values_default_sources.clone())
104        .with_guarded_values_default_sources(guarded_values_default_sources)
105        .with_scoped_guarded_root_overlay_requirement_implications(guarded_root_overlays)
106        .with_values_program_wrappers(values_program_wrappers)
107        .with_values_program_wrapper_exclusions(values_program_wrapper_exclusions);
108
109        Self {
110            uses: normalized_uses,
111            schema_signals,
112        }
113    }
114
115    /// Returns normalized contract uses in stable inspection order.
116    #[must_use]
117    pub fn uses(&self) -> &[ContractUse] {
118        &self.uses
119    }
120
121    /// Returns path-local facts prepared for schema lowering.
122    #[must_use]
123    pub fn schema_signals(&self) -> &ContractSchemaSignals {
124        &self.schema_signals
125    }
126
127    /// Builds the versioned inspection document for this contract.
128    #[must_use]
129    pub fn document(&self) -> ContractDocument {
130        ContractDocument::from_contract_uses(self.uses.clone())
131    }
132
133    /// Consumes the contract and returns its schema-lowering signals.
134    #[must_use]
135    pub fn into_schema_signals(self) -> ContractSchemaSignals {
136        self.schema_signals
137    }
138}