use super::{
BTreeMap, BTreeSet, ConditionalGuard, ContractPathAccumulator, ContractRequirementImplication,
ContractRequirementTarget, ContractUse, FailValueRequirement, Guard, Predicate,
ProviderSchemaUse, ValueKind, ValuesPath, path_accumulator,
};
pub(super) fn hard_negation_paths(predicate: &Predicate, out: &mut BTreeSet<String>) {
match predicate {
Predicate::Not(inner) => {
if !matches!(inner.as_ref(), Predicate::Guard(Guard::Absent { .. })) {
out.extend(inner.value_paths().into_iter().map(|path| path.encode()));
}
}
Predicate::And(items) | Predicate::Or(items) => {
for item in items {
hard_negation_paths(item, out);
}
}
_ => {}
}
}
pub(super) fn lowerable_conditional_guard_set(
contract_use: &ContractUse,
predicates: &[Predicate],
) -> Option<Vec<ConditionalGuard>> {
let source_path = contract_use.source_expr.clone();
let source_expr = source_path.encode();
let key_equals_ranges: BTreeSet<&helm_schema_core::ValuesPath> = predicates
.iter()
.filter_map(|predicate| match predicate {
Predicate::Guard(Guard::RangeKeyEquals { path, .. }) => Some(path),
_ => None,
})
.collect();
let mut guards = Vec::new();
for predicate in predicates {
if matches!(
predicate,
Predicate::Guard(Guard::Range { path })
if path == &source_path
|| range_guard_is_iteration_ancestor(&source_expr, path)
|| key_equals_ranges.contains(path)
) {
continue;
}
extend_lowerable_predicate(predicate, &source_expr, &mut guards)?;
}
guards.sort();
guards.dedup();
Some(guards)
}
pub(super) fn collapse_layered_truthy_gates<'a>(
guards: Vec<ConditionalGuard>,
layers: impl IntoIterator<Item = &'a helm_schema_core::ValuesPath>,
) -> Vec<ConditionalGuard> {
let split: Vec<Vec<helm_schema_core::Segment>> = layers
.into_iter()
.map(|layer| layer.segments().cloned().collect())
.collect();
let Some(first) = split.first() else {
return guards;
};
let mut common = 0;
'suffix: while common < first.len() {
let candidate = first
.len()
.checked_sub(1 + common)
.and_then(|index| first.get(index));
let Some(candidate) = candidate else {
break;
};
for segments in &split {
let segment = segments
.len()
.checked_sub(1 + common)
.filter(|&index| index > 0)
.and_then(|index| segments.get(index));
if segment != Some(candidate) {
break 'suffix;
}
}
common += 1;
}
let roots: Vec<helm_schema_core::ValuesPath> = split
.iter()
.map(|segments| {
let keep = segments.len().saturating_sub(common);
helm_schema_core::ValuesPath::from_segments(
segments.get(..keep).unwrap_or_default().iter().cloned(),
)
})
.collect();
let concrete_layers: Vec<&helm_schema_core::ValuesPath> = roots
.iter()
.filter(|layer| !values_path_contains_wildcard(layer))
.collect();
let wildcard_collections: Vec<helm_schema_core::ValuesPath> = roots
.iter()
.filter_map(|layer| {
let segments = layer.segments().collect::<Vec<_>>();
let wildcard = segments
.iter()
.position(|segment| segment.is_each_member())?;
let prefix = segments.get(..wildcard)?;
(!prefix.is_empty())
.then(|| helm_schema_core::ValuesPath::from_segments(prefix.iter().copied()))
})
.collect();
let mut suffix_members: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
for (index, guard) in guards.iter().enumerate() {
let ConditionalGuard::Truthy { path } = guard else {
continue;
};
for layer in &concrete_layers {
if let Some(suffix) = layered_guard_suffix(path, layer) {
suffix_members.entry(suffix).or_default().insert(index);
}
}
}
let mut dropped = BTreeSet::new();
let mut replacements = Vec::new();
for members in suffix_members.into_values() {
if members.len() < 2 || members.iter().any(|index| dropped.contains(index)) {
continue;
}
dropped.extend(members.iter().copied());
let mut arms: Vec<ConditionalGuard> = members
.iter()
.filter_map(|&index| guards.get(index).cloned())
.collect();
arms.extend(
wildcard_collections
.iter()
.map(|path| ConditionalGuard::Truthy { path: path.clone() }),
);
arms.sort();
arms.dedup();
replacements.push(ConditionalGuard::AnyOf(arms));
}
if dropped.is_empty() {
return guards;
}
let mut out: Vec<ConditionalGuard> = guards
.into_iter()
.enumerate()
.filter(|(index, _)| !dropped.contains(index))
.map(|(_, guard)| guard)
.collect();
out.extend(replacements);
out.sort();
out.dedup();
out
}
fn layered_guard_suffix(
path: &helm_schema_core::ValuesPath,
layer: &helm_schema_core::ValuesPath,
) -> Option<String> {
if !path.is_descendant_of(layer) {
return None;
}
Some(
helm_schema_core::ValuesPath::from_segments(path.segments().skip(layer.segments().len()))
.encode(),
)
}
pub(super) fn lowerable_conditional_guard_subset(
contract_use: &ContractUse,
predicates: &[Predicate],
) -> Vec<ConditionalGuard> {
let source_path = contract_use.source_expr.clone();
let source_expr = source_path.encode();
let key_equals_ranges: BTreeSet<&helm_schema_core::ValuesPath> = predicates
.iter()
.filter_map(|predicate| match predicate {
Predicate::Guard(Guard::RangeKeyEquals { path, .. }) => Some(path),
_ => None,
})
.collect();
let mut guards = Vec::new();
for predicate in predicates {
if matches!(
predicate,
Predicate::Guard(Guard::Range { path })
if path == &source_path
|| range_guard_is_iteration_ancestor(&source_expr, path)
|| key_equals_ranges.contains(path)
) {
continue;
}
let mut lowered = Vec::new();
if extend_lowerable_predicate(predicate, &source_expr, &mut lowered).is_some() {
guards.extend(lowered);
}
}
guards.sort();
guards.dedup();
guards
}
pub(super) fn provider_schema_use(
contract_use: &ContractUse,
self_range_guarded: bool,
source_null_tolerant: bool,
) -> Option<ProviderSchemaUse> {
let nil_omitting_ranged_leaf = contract_use.kind == ValueKind::Serialized
&& contract_use.nil_omitting
&& values_path_contains_wildcard(&contract_use.source_expr);
if contract_use.source_expr.segments().next().is_none()
|| (matches!(
contract_use.kind,
ValueKind::PartialScalar | ValueKind::Serialized
) && !nil_omitting_ranged_leaf)
|| (contract_use.stringified
&& !matches!(
contract_use.kind,
ValueKind::Scalar | ValueKind::YamlSerialized | ValueKind::TemplatedYamlSerialized
)
&& !nil_omitting_ranged_leaf)
|| contract_use.path.0.is_empty()
{
return None;
}
let resource = contract_use.resource.clone()?;
Some(ProviderSchemaUse {
value_path: contract_use.source_expr.clone(),
path: contract_use.path.clone(),
kind: contract_use.kind,
stringified: contract_use.stringified,
resource,
template_supplied_member_keys: contract_use.template_supplied_member_keys.clone(),
split_segment: contract_use.split_segment.clone(),
merge_layers: contract_use.merge_layers.clone(),
range_key: contract_use.range_key,
nil_omitting: contract_use.nil_omitting,
omitted_members: contract_use
.omitted_members
.iter()
.map(|(key, retain_guards)| {
let guards = retain_guards
.iter()
.map(|guard| guard_to_conditional_guard(guard, None))
.collect::<Option<Vec<_>>>()
.unwrap_or_default();
(key.clone(), guards)
})
.collect(),
is_self_range_collection: self_range_guarded
&& contract_use
.path
.0
.last()
.is_none_or(|segment| !segment.ends_with("[*]")),
source_null_tolerant,
outer_guards: Vec::new(),
})
}
pub(super) fn predicate_to_guard(
predicate: &Predicate,
target_value_path: Option<&str>,
) -> Option<ConditionalGuard> {
match predicate {
Predicate::True | Predicate::False | Predicate::Approximate { .. } => None,
Predicate::Guard(guard) => guard_to_conditional_guard(guard, target_value_path),
Predicate::Not(inner) => {
if matches!(
inner.as_ref(),
Predicate::Guard(Guard::RangeKeyEquals { .. })
) {
return None;
}
Some(ConditionalGuard::Not(Box::new(predicate_to_guard(
inner, None,
)?)))
}
Predicate::And(predicates) => {
let mut guards = predicates
.iter()
.map(|predicate| predicate_to_guard(predicate, target_value_path))
.collect::<Option<Vec<_>>>()?;
guards.sort();
guards.dedup();
match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AllOf(guards)),
}
}
Predicate::Or(predicates) => {
let mut guards = predicates
.iter()
.map(|predicate| predicate_to_guard(predicate, None))
.collect::<Option<Vec<_>>>()?;
if guards
.iter()
.flat_map(ConditionalGuard::value_paths)
.any(|path| path_contains_wildcard(&path.encode()))
{
return None;
}
guards.sort();
guards.dedup();
(target_value_path.is_some() || !guards.is_empty())
.then_some(ConditionalGuard::AnyOf(guards))
}
}
}
pub(super) fn extend_lowerable_predicate(
predicate: &Predicate,
target_value_path: &str,
out: &mut Vec<ConditionalGuard>,
) -> Option<()> {
let target = helm_schema_core::ValuesPath::parse(target_value_path);
match predicate {
Predicate::True
| Predicate::False
| Predicate::Approximate { .. }
| Predicate::Guard(Guard::Range { .. }) => return None,
Predicate::Guard(Guard::With { path }) if path == &target => {}
Predicate::Guard(Guard::With { .. }) => {
out.push(predicate_to_guard(predicate, None)?);
}
Predicate::And(predicates) => {
for predicate in predicates {
extend_lowerable_predicate(predicate, target_value_path, out)?;
}
}
Predicate::Guard(Guard::Default { path }) if path == &target => {}
Predicate::Guard(Guard::Truthy { path }) if path == &target => {}
Predicate::Not(inner)
if matches!(
inner.as_ref(),
Predicate::Guard(Guard::Truthy { path }) if path == &target
) => {}
other if predicate_is_self_type_partition(other, target_value_path) => {
let target = if path_contains_wildcard(target_value_path) {
None
} else {
Some(target_value_path)
};
out.push(predicate_to_guard(other, target)?);
}
other => {
out.push(predicate_to_guard(other, Some(target_value_path))?);
}
}
Some(())
}
pub(super) fn terminal_clause_guard(predicate: &Predicate) -> Option<ConditionalGuard> {
if let Predicate::Guard(Guard::Range { path }) = predicate {
return Some(ConditionalGuard::Truthy { path: path.clone() });
}
if let Predicate::Approximate {
sound_subset: Some(sound_subset),
..
} = predicate
{
return predicate_to_guard(sound_subset, None);
}
if let Predicate::Or(items) = predicate
&& predicate.contains_approximation()
{
let mut guards = items
.iter()
.map(terminal_clause_guard)
.collect::<Option<Vec<_>>>()?;
guards.sort();
guards.dedup();
return match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AnyOf(guards)),
};
}
if let Predicate::And(items) = predicate
&& predicate.contains_approximation()
{
let mut guards = items
.iter()
.map(terminal_clause_guard)
.collect::<Option<Vec<_>>>()?;
guards.sort();
guards.dedup();
return match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AllOf(guards)),
};
}
predicate_to_guard(predicate, None)
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(super) fn guard_to_conditional_guard(
guard: &Guard,
target_value_path: Option<&str>,
) -> Option<ConditionalGuard> {
let path = |path: &helm_schema_core::ValuesPath| match target_value_path {
Some(target_value_path) => lowerable_guard_path(path, target_value_path),
None => Some(path.clone()),
};
match guard {
Guard::Truthy { path: value_path } => Some(ConditionalGuard::Truthy {
path: path(value_path)?,
}),
Guard::With { path: value_path } if target_value_path.is_none() => {
Some(ConditionalGuard::With {
path: path(value_path)?,
})
}
Guard::Eq {
path: value_path,
value,
} => Some(ConditionalGuard::Eq {
path: path(value_path)?,
value: value.clone(),
}),
Guard::NotEq {
path: value_path,
value,
} => Some(ConditionalGuard::NotEq {
path: path(value_path)?,
value: value.clone(),
}),
Guard::Absent { path: value_path } => Some(ConditionalGuard::Absent {
path: path(value_path)?,
}),
Guard::HasKey {
path: value_path,
key,
} => Some(ConditionalGuard::HasKey {
path: path(value_path)?,
key: key.clone(),
}),
Guard::NotHasKey {
path: value_path,
key,
} => Some(ConditionalGuard::Not(Box::new(ConditionalGuard::HasKey {
path: path(value_path)?,
key: key.clone(),
}))),
Guard::ContainsMemberEquals {
path: value_path,
member,
value,
} => Some(ConditionalGuard::ContainsMemberEquals {
path: path(value_path)?,
member: member.clone(),
value: value.clone(),
}),
Guard::ContainsTruthyMember {
path: value_path,
member,
} => Some(ConditionalGuard::ContainsTruthyMember {
path: path(value_path)?,
member: member.clone(),
}),
Guard::ContainsEquals {
path: value_path,
value,
} => Some(ConditionalGuard::ContainsEquals {
path: path(value_path)?,
value: value.clone(),
}),
Guard::MatchesPattern {
path: value_path,
pattern,
templated: false,
} => Some(ConditionalGuard::MatchesPattern {
path: path(value_path)?,
pattern: pattern.clone(),
}),
Guard::NotMatchesPattern {
path: value_path,
pattern,
} => {
let value_path = path(value_path)?;
Some(ConditionalGuard::AllOf(vec![
ConditionalGuard::TypeIs {
path: value_path.clone(),
schema_type: "string".to_string(),
},
ConditionalGuard::Not(Box::new(ConditionalGuard::MatchesPattern {
path: value_path,
pattern: pattern.clone(),
})),
]))
}
Guard::MatchesPattern { .. }
| Guard::RangeKeyPrefix { .. }
| Guard::RangeKeyMatches { .. }
| Guard::Range { .. }
| Guard::With { .. }
| Guard::Default { .. } => None,
Guard::AtMostOneMember { path: value_path } => Some(ConditionalGuard::AtMostOneMember {
path: path(value_path)?,
}),
Guard::MinMembers {
path: value_path,
bound,
} => {
let path = if target_value_path
.is_some_and(|target| value_path == &helm_schema_core::ValuesPath::parse(target))
{
(!values_path_contains_wildcard(value_path)).then(|| value_path.clone())?
} else {
path(value_path)?
};
Some(ConditionalGuard::MinMembers {
path,
bound: *bound,
})
}
Guard::TypeIs {
path: value_path,
schema_type,
} => {
let path = if target_value_path
.is_some_and(|target| value_path == &helm_schema_core::ValuesPath::parse(target))
{
(!values_path_contains_wildcard(value_path)).then(|| value_path.clone())?
} else {
path(value_path)?
};
Some(ConditionalGuard::TypeIs {
path,
schema_type: schema_type.clone(),
})
}
Guard::NotTypeIs {
path: value_path,
schema_type,
} => {
let path = if target_value_path
.is_some_and(|target| value_path == &helm_schema_core::ValuesPath::parse(target))
{
(!values_path_contains_wildcard(value_path)).then(|| value_path.clone())?
} else {
path(value_path)?
};
Some(ConditionalGuard::Not(Box::new(ConditionalGuard::TypeIs {
path,
schema_type: schema_type.clone(),
})))
}
Guard::IntGt {
path: value_path,
bound,
} => Some(ConditionalGuard::IntGt {
path: path(value_path)?,
bound: *bound,
}),
Guard::IntLt {
path: value_path,
bound,
} => Some(ConditionalGuard::IntLt {
path: path(value_path)?,
bound: *bound,
}),
Guard::RangeKeyEquals {
path: value_path,
key,
} => {
if key.is_empty() {
return None;
}
Some(ConditionalGuard::HasKey {
path: path(value_path)?,
key: key.clone(),
})
}
Guard::Not { path: value_path } => {
Some(ConditionalGuard::Not(Box::new(ConditionalGuard::Truthy {
path: path(value_path)?,
})))
}
Guard::Or { paths } => Some(ConditionalGuard::AnyOf(
paths
.iter()
.map(|value_path| {
Some(ConditionalGuard::Truthy {
path: path(value_path)?,
})
})
.collect::<Option<Vec<_>>>()?,
)),
Guard::AnyOf { alternatives } => Some(ConditionalGuard::AnyOf(
alternatives
.iter()
.map(|alternative| {
let mut guards = alternative
.iter()
.map(|guard| guard_to_conditional_guard(guard, target_value_path))
.collect::<Option<Vec<_>>>()?;
guards.sort();
guards.dedup();
match guards.as_slice() {
[] => None,
[guard] => Some(guard.clone()),
_ => Some(ConditionalGuard::AllOf(guards)),
}
})
.collect::<Option<Vec<_>>>()?,
)),
}
}
pub(super) fn record_member_range_requirement(
paths: &mut BTreeMap<ValuesPath, ContractPathAccumulator>,
parent: &ValuesPath,
predicates: &[Predicate],
outer_allows_integer: bool,
inner_allows_integer: bool,
) {
let mut member_path = parent.clone();
member_path.push_each_member();
let mut outer_guards = Vec::new();
for predicate in predicates {
if matches!(
predicate,
Predicate::Guard(Guard::Range { path })
if path == parent || path == &member_path
) {
continue;
}
let Some(guard) = predicate_to_guard(predicate, None) else {
return;
};
if guard
.value_paths()
.iter()
.any(|path| path_contains_wildcard(&path.encode()))
{
return;
}
outer_guards.push(guard);
}
outer_guards.sort();
outer_guards.dedup();
let implication = ContractRequirementImplication {
outer_guards,
target: ContractRequirementTarget::Members {
allow_integer: outer_allows_integer,
},
requirements: vec![FailValueRequirement::Iterable {
allow_integer: inner_allows_integer,
}],
};
let acc = path_accumulator(paths, parent);
acc.referenced = true;
if !acc.requirement_implications.contains(&implication) {
acc.requirement_implications.push(implication);
}
}
pub(super) fn predicate_tests_source_type(predicate: &Predicate, source_expr: &str) -> bool {
let source_path = helm_schema_core::ValuesPath::parse(source_expr);
match predicate {
Predicate::Guard(Guard::TypeIs { path, .. }) => path == &source_path,
Predicate::Not(inner) => predicate_tests_source_type(inner, source_expr),
Predicate::And(items) | Predicate::Or(items) => items
.iter()
.any(|item| predicate_tests_source_type(item, source_expr)),
Predicate::True
| Predicate::False
| Predicate::Approximate { .. }
| Predicate::Guard(_) => false,
}
}
pub(super) fn predicate_is_self_type_partition(
predicate: &Predicate,
target_value_path: &str,
) -> bool {
let target = helm_schema_core::ValuesPath::parse(target_value_path);
match predicate {
Predicate::Guard(Guard::TypeIs { path, .. }) => path == &target,
Predicate::Not(inner) => predicate_is_self_type_partition(inner, target_value_path),
Predicate::And(items) | Predicate::Or(items) => {
!items.is_empty()
&& items
.iter()
.all(|item| predicate_is_self_type_partition(item, target_value_path))
}
Predicate::True
| Predicate::False
| Predicate::Approximate { .. }
| Predicate::Guard(_) => false,
}
}
pub(super) fn lowerable_guard_path(
path: &helm_schema_core::ValuesPath,
target_value_path: &str,
) -> Option<helm_schema_core::ValuesPath> {
let target = helm_schema_core::ValuesPath::parse(target_value_path);
if path == &target {
return None;
}
if !values_path_contains_wildcard(path) {
return Some(path.clone());
}
let path_segments = path.segments().collect::<Vec<_>>();
let target = helm_schema_core::ValuesPath::parse(target_value_path);
let target_segments = target.segments().collect::<Vec<_>>();
let last_wildcard = path_segments
.iter()
.rposition(|segment| segment.is_each_member())?;
let member_prefix = path_segments.get(..=last_wildcard)?;
target_segments
.get(..=last_wildcard)
.is_some_and(|segments| segments == member_prefix)
.then(|| path.clone())
}
pub(super) fn path_contains_wildcard(path: &str) -> bool {
helm_schema_core::ValuesPath::parse(path)
.segments()
.any(helm_schema_core::Segment::is_each_member)
}
fn values_path_contains_wildcard(path: &helm_schema_core::ValuesPath) -> bool {
path.segments()
.any(helm_schema_core::Segment::is_each_member)
}
pub(super) fn member_local_truthy_selector(
collection: &str,
predicate: &Predicate,
constrained: Option<&str>,
) -> Option<Vec<String>> {
let Predicate::Guard(Guard::Truthy { path } | Guard::With { path }) = predicate else {
return None;
};
if constrained
.is_some_and(|constrained| path == &helm_schema_core::ValuesPath::parse(constrained))
{
return None;
}
let collection = helm_schema_core::ValuesPath::parse(collection);
let path_segments = path.segments().collect::<Vec<_>>();
let collection_segments = collection.segments().collect::<Vec<_>>();
let suffix = path_segments.strip_prefix(collection_segments.as_slice())?;
let (member, suffix) = suffix.split_first()?;
if !member.is_each_member()
|| suffix.is_empty()
|| suffix.iter().any(|segment| segment.is_each_member())
{
return None;
}
suffix
.iter()
.map(|segment| segment.literal().map(str::to_string))
.collect()
}
pub(super) fn ranged_member_parent(path: &str) -> Option<&str> {
path.strip_suffix(".*")
.or_else(|| path.split_once(".*.").map(|(parent, _)| parent))
}
pub(super) fn range_guard_is_iteration_ancestor(
source_path: &str,
guard_path: &helm_schema_core::ValuesPath,
) -> bool {
let source = helm_schema_core::ValuesPath::parse(source_path);
let source_segments = source.segments().collect::<Vec<_>>();
let guard_segments = guard_path.segments().collect::<Vec<_>>();
source_segments.len() > guard_segments.len()
&& source_segments
.iter()
.copied()
.zip(guard_segments.iter().copied())
.all(|(source, guard)| source == guard)
&& source_segments
.get(guard_segments.len())
.is_some_and(|segment| segment.is_each_member())
}
pub(super) fn predicate_is_structural_ancestor_guard(
predicate: &Predicate,
source_path: &str,
) -> bool {
let Predicate::Guard(Guard::Truthy { path } | Guard::With { path }) = predicate else {
return false;
};
let source = helm_schema_core::ValuesPath::parse(source_path);
let source_segments = source.segments().collect::<Vec<_>>();
let guard_segments = path.segments().collect::<Vec<_>>();
source_segments.len() > guard_segments.len()
&& source_segments
.iter()
.copied()
.zip(guard_segments.iter().copied())
.all(|(source, guard)| source == guard)
}
pub(super) fn collect_paths_with_descendants(
paths: &BTreeSet<ValuesPath>,
) -> (
BTreeSet<ValuesPath>,
BTreeSet<ValuesPath>,
BTreeSet<ValuesPath>,
) {
let mut ancestors = BTreeSet::new();
let mut item_ancestors = BTreeSet::new();
let mut structured_item_ancestors = BTreeSet::new();
for path in paths {
let segments = path.segments().collect::<Vec<_>>();
for prefix_len in 1..segments.len() {
let Some(prefix) = segments.get(..prefix_len) else {
continue;
};
let Some(segment) = segments.get(prefix_len) else {
continue;
};
let ancestor = ValuesPath::from_segments(prefix.iter().copied());
if segment.is_each_member() {
item_ancestors.insert(ancestor.clone());
if prefix_len + 1 < segments.len() {
structured_item_ancestors.insert(ancestor.clone());
}
}
ancestors.insert(ancestor);
}
}
(ancestors, item_ancestors, structured_item_ancestors)
}