use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use crate::{ContractProvenance, ValueKind};
use helm_schema_core::{GuardValue, Predicate};
#[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) defaulted: bool,
pub(crate) shape_erased: bool,
pub(crate) nil_omitted: bool,
pub(crate) stringified: bool,
pub(crate) yaml_serialized: bool,
pub(crate) derived_text: bool,
pub(crate) partial_text: bool,
pub(crate) string_contract: bool,
pub(crate) json_serialized: bool,
pub(crate) json_decoded: bool,
pub(crate) nil_scrubbed: bool,
pub(crate) merge_layers: Option<helm_schema_core::MergeLayersUse>,
pub(crate) provenance: Vec<ContractProvenance>,
pub(crate) suppress_predicate_paths: BTreeSet<String>,
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 merge(&mut self, other: &Self) {
self.predicates.extend(other.predicates.iter().cloned());
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.derived_text |= other.derived_text;
self.partial_text |= other.partial_text;
self.string_contract |= other.string_contract;
self.json_serialized |= other.json_serialized;
self.json_decoded |= other.json_decoded;
self.nil_scrubbed |= other.nil_scrubbed;
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: impl Into<String>) {
let path = path.into();
if !path.is_empty() {
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: String,
pub(crate) kind: ValueKind,
pub(crate) encoded: bool,
pub(crate) meta: HelperOutputMeta,
}
pub(crate) fn merge_rendered_row_meta(
output_meta: &mut BTreeMap<String, 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 insert_type_hint(
hints: &mut BTreeMap<String, BTreeSet<String>>,
path: String,
schema_type: &str,
) {
if path.trim().is_empty() {
return;
}
hints
.entry(path)
.or_default()
.insert(schema_type.to_string());
}
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) => 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, "(?:{})?", regex_literal(token));
}
let _ = write!(composed, "(?:{anchored})");
if let Some(token) = suffix {
let _ = write!(composed, "(?:{})?", regex_literal(token));
}
if let Some(token) = cut {
let _ = write!(composed, "(?:{}.*)?", regex_literal(token));
}
composed.push('$');
Some(composed)
}
fn regex_literal(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
for character in text.chars() {
if matches!(
character,
'.' | '+'
| '*'
| '?'
| '('
| ')'
| '|'
| '['
| ']'
| '{'
| '}'
| '^'
| '$'
| '\\'
| '/'
) {
escaped.push('\\');
}
escaped.push(character);
}
escaped
}