use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use crate::{ContractProvenance, ValueKind};
use helm_schema_core::{GuardValue, Predicate, ValuesPath};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct EmptyRescue {
pub(crate) fallback: String,
pub(crate) spellings: BTreeSet<GuardValue>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum LexicalEscape {
Contains(String),
TrimPrefix(String),
TrimSuffix(String),
CutAtToken(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct HelperOutputMeta {
pub(crate) predicates: BTreeSet<BTreeSet<Predicate>>,
pub(crate) input_identity: bool,
pub(crate) defaulted: bool,
pub(crate) shape_erased: bool,
pub(crate) nil_omitted: bool,
pub(crate) stringified: bool,
pub(crate) yaml_serialized: bool,
pub(crate) templated_yaml: bool,
pub(crate) derived_text: bool,
pub(crate) partial_text: bool,
pub(crate) plain_slot_string_format: bool,
pub(crate) json_serialized: bool,
pub(crate) json_decoded: bool,
pub(crate) nil_scrubbed: bool,
pub(crate) parsed_map: bool,
pub(crate) merge_layers: Option<helm_schema_core::MergeLayersUse>,
pub(crate) provenance: Vec<ContractProvenance>,
pub(crate) suppress_predicate_paths: BTreeSet<ValuesPath>,
pub(crate) capture_exclusions: BTreeSet<Predicate>,
pub(crate) lexical_escapes: BTreeSet<LexicalEscape>,
pub(crate) omitted_keys: std::collections::BTreeMap<String, Vec<crate::Guard>>,
pub(crate) empty_fold_spellings: Option<BTreeSet<GuardValue>>,
pub(crate) empty_rescue: Option<EmptyRescue>,
pub(crate) default_fallback: Option<GuardValue>,
}
impl HelperOutputMeta {
pub(crate) fn is_input_identity(&self) -> bool {
self.input_identity && !self.defaulted && self.is_structurally_untransformed()
}
pub(crate) fn is_input_member_identity(&self) -> bool {
let Self {
predicates: _,
input_identity,
defaulted: _,
shape_erased,
nil_omitted,
stringified,
yaml_serialized,
templated_yaml,
derived_text,
partial_text,
plain_slot_string_format,
json_serialized,
json_decoded,
nil_scrubbed,
parsed_map,
merge_layers,
provenance: _,
suppress_predicate_paths: _,
capture_exclusions: _,
lexical_escapes,
omitted_keys: _,
empty_fold_spellings,
empty_rescue,
default_fallback: _,
} = self;
*input_identity
&& !shape_erased
&& !nil_omitted
&& !stringified
&& !yaml_serialized
&& !templated_yaml
&& !derived_text
&& !partial_text
&& !plain_slot_string_format
&& !json_serialized
&& !json_decoded
&& !nil_scrubbed
&& !parsed_map
&& merge_layers.is_none()
&& lexical_escapes.is_empty()
&& empty_fold_spellings.is_none()
&& empty_rescue.is_none()
}
pub(crate) fn is_identity_preserving_default(&self) -> bool {
self.defaulted && self.is_structurally_untransformed()
}
fn is_structurally_untransformed(&self) -> bool {
let Self {
predicates: _,
input_identity: _,
defaulted: _,
shape_erased,
nil_omitted,
stringified,
yaml_serialized,
templated_yaml,
derived_text,
partial_text,
plain_slot_string_format,
json_serialized,
json_decoded,
nil_scrubbed,
parsed_map,
merge_layers,
provenance: _,
suppress_predicate_paths: _,
capture_exclusions: _,
lexical_escapes,
omitted_keys,
empty_fold_spellings,
empty_rescue,
default_fallback: _,
} = self;
!shape_erased
&& !nil_omitted
&& !stringified
&& !yaml_serialized
&& !templated_yaml
&& !derived_text
&& !partial_text
&& !plain_slot_string_format
&& !json_serialized
&& !json_decoded
&& !nil_scrubbed
&& !parsed_map
&& merge_layers.is_none()
&& omitted_keys.is_empty()
&& lexical_escapes.is_empty()
&& empty_fold_spellings.is_none()
&& empty_rescue.is_none()
}
pub(crate) fn merge(&mut self, other: &Self) {
self.predicates.extend(other.predicates.iter().cloned());
self.input_identity |= other.input_identity;
self.defaulted |= other.defaulted;
self.shape_erased |= other.shape_erased;
self.nil_omitted |= other.nil_omitted;
self.stringified |= other.stringified;
self.yaml_serialized |= other.yaml_serialized;
self.templated_yaml |= other.templated_yaml;
self.derived_text |= other.derived_text;
self.partial_text |= other.partial_text;
self.plain_slot_string_format |= other.plain_slot_string_format;
self.json_serialized |= other.json_serialized;
self.json_decoded |= other.json_decoded;
self.nil_scrubbed |= other.nil_scrubbed;
self.parsed_map |= other.parsed_map;
self.merge_layers = merge_exact_fact(self.merge_layers.take(), other.merge_layers.clone());
merge_provenance_sites(&mut self.provenance, &other.provenance);
self.suppress_predicate_paths
.extend(other.suppress_predicate_paths.iter().cloned());
self.capture_exclusions
.extend(other.capture_exclusions.iter().cloned());
self.lexical_escapes
.extend(other.lexical_escapes.iter().cloned());
for (key, retain_guards) in &other.omitted_keys {
self.omitted_keys
.entry(key.clone())
.and_modify(|existing| {
if existing != retain_guards {
existing.clear();
}
})
.or_insert_with(|| retain_guards.clone());
}
self.empty_fold_spellings = merge_exact_fact(
self.empty_fold_spellings.take(),
other.empty_fold_spellings.clone(),
);
self.empty_rescue = merge_exact_fact(self.empty_rescue.take(), other.empty_rescue.clone());
self.default_fallback =
merge_exact_fact(self.default_fallback.take(), other.default_fallback.clone());
}
pub(crate) fn suppress_predicate_path(&mut self, path: ValuesPath) {
if path.segments().next().is_some() {
self.suppress_predicate_paths.insert(path);
}
}
pub(crate) fn conjoin_branches(&mut self, predicates: &BTreeSet<Predicate>) {
if predicates.is_empty() {
return;
}
if self.predicates.is_empty() {
self.predicates.insert(predicates.clone());
return;
}
self.predicates = std::mem::take(&mut self.predicates)
.into_iter()
.map(|mut branch| {
branch.extend(predicates.iter().cloned());
branch
})
.collect();
}
}
fn merge_exact_fact<T: PartialEq>(left: Option<T>, right: Option<T>) -> Option<T> {
match (left, right) {
(Some(left), Some(right)) => (left == right).then_some(left),
(left, right) => left.or(right),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RenderedRow {
pub(crate) path: ValuesPath,
pub(crate) kind: ValueKind,
pub(crate) encoded: bool,
pub(crate) meta: HelperOutputMeta,
}
pub(crate) fn merge_rendered_row_meta(
output_meta: &mut BTreeMap<ValuesPath, HelperOutputMeta>,
rows: &[RenderedRow],
) {
for row in rows {
output_meta
.entry(row.path.clone())
.or_default()
.merge(&row.meta);
}
}
pub(crate) fn merge_provenance_sites(
target: &mut Vec<ContractProvenance>,
extra: &[ContractProvenance],
) {
for site in extra {
if !target.contains(site) {
target.push(site.clone());
}
}
}
pub(crate) fn values_paths_are_related(left: &str, right: &str) -> bool {
let left_root = helm_schema_core::split_value_path(left).into_iter().next();
let right_root = helm_schema_core::split_value_path(right).into_iter().next();
left_root == right_root
|| helm_schema_core::values_path_is_descendant(left, right)
|| helm_schema_core::values_path_is_descendant(right, left)
}
pub(crate) fn pattern_with_lexical_escapes(
pattern: &str,
escapes: &BTreeSet<LexicalEscape>,
) -> String {
if escapes.is_empty() {
return pattern.to_string();
}
if let Some(composed) = composed_edge_escape_pattern(pattern, escapes) {
return composed;
}
let mut alternatives: Vec<String> = escapes
.iter()
.map(|escape| match escape {
LexicalEscape::Contains(token)
| LexicalEscape::TrimPrefix(token)
| LexicalEscape::TrimSuffix(token)
| LexicalEscape::CutAtToken(token) => crate::escape_regex_literal(token),
})
.collect();
alternatives.push(format!("(?:{pattern})"));
alternatives.join("|")
}
fn composed_edge_escape_pattern(
pattern: &str,
escapes: &BTreeSet<LexicalEscape>,
) -> Option<String> {
let anchored = pattern
.strip_prefix('^')
.and_then(|p| p.strip_suffix('$'))?;
let mut prefix = None;
let mut suffix = None;
let mut cut = None;
for escape in escapes {
let slot = match escape {
LexicalEscape::Contains(_) => return None,
LexicalEscape::TrimPrefix(token) => (&mut prefix, token),
LexicalEscape::TrimSuffix(token) => (&mut suffix, token),
LexicalEscape::CutAtToken(token) => (&mut cut, token),
};
if slot.0.replace(slot.1).is_some() {
return None;
}
}
let mut composed = String::from("^");
if let Some(token) = prefix {
let _ = write!(composed, "(?:{})?", crate::escape_regex_literal(token));
}
let _ = write!(composed, "(?:{anchored})");
if let Some(token) = suffix {
let _ = write!(composed, "(?:{})?", crate::escape_regex_literal(token));
}
if let Some(token) = cut {
let _ = write!(composed, "(?:{}.*)?", crate::escape_regex_literal(token));
}
composed.push('$');
Some(composed)
}