use std::collections::BTreeSet;
use crate::ValueKind;
use crate::abstract_value::{AbstractValue, path_is_encoded};
use crate::helper_meta::HelperOutputMeta;
use helm_schema_core::Predicate;
use super::domain::{
AbstractFragment, AbstractString, EntryKey, Guarded, Mapping, MappingEntry, Opaque,
PathCondition, Sequence, Splice, SpliceMeta, StringPart, TaintPart,
};
pub(crate) const MAX_SCALAR_ARMS: usize = 8;
pub(crate) const MAX_SCALAR_ARM_FANOUT: usize = 64;
pub(crate) struct LowerScope<'a> {
pub(crate) defaulted_paths: &'a BTreeSet<String>,
pub(crate) encoded_paths: &'a BTreeSet<String>,
pub(crate) derived_text_paths: &'a BTreeSet<String>,
pub(crate) merge_operand_paths: &'a BTreeSet<String>,
pub(crate) yaml_serialized_paths: &'a BTreeSet<String>,
pub(crate) shape_erased_paths: &'a BTreeSet<String>,
pub(crate) nil_omitting_paths: &'a BTreeSet<String>,
pub(crate) string_contract_paths: &'a BTreeSet<String>,
pub(crate) json_serialized_paths: &'a BTreeSet<String>,
pub(crate) chart_value_defaults: &'a BTreeSet<String>,
pub(crate) local_source_paths: &'a BTreeSet<String>,
pub(crate) local_output_meta: &'a std::collections::BTreeMap<String, HelperOutputMeta>,
}
impl LowerScope<'_> {
pub(super) fn splice(
&self,
path: &str,
kind: ValueKind,
helper_meta: Option<&HelperOutputMeta>,
) -> Splice {
let defaulted = helper_meta.is_some_and(|meta| meta.defaulted)
|| self.defaulted_paths.contains(path)
|| self.chart_value_defaults.contains(path);
Splice {
values_path: path.to_string(),
kind,
meta: SpliceMeta {
defaulted,
encoded: path_is_encoded(path, self.encoded_paths),
shape_erased: helper_meta.is_some_and(|meta| meta.shape_erased)
|| path_is_encoded(path, self.shape_erased_paths),
nil_omitted: helper_meta.is_some_and(|meta| meta.nil_omitted)
|| path_is_encoded(path, self.nil_omitting_paths),
yaml_serialized: helper_meta.is_some_and(|meta| meta.yaml_serialized)
|| path_is_encoded(path, self.yaml_serialized_paths),
string_contract: helper_meta.is_some_and(|meta| meta.string_contract)
|| path_is_encoded(path, self.string_contract_paths),
json_serialized: helper_meta.is_some_and(|meta| meta.json_serialized)
|| path_is_encoded(path, self.json_serialized_paths),
json_decoded: helper_meta.is_some_and(|meta| meta.json_decoded),
lexical_escapes: helper_meta
.map(|meta| meta.lexical_escapes.clone())
.unwrap_or_default(),
split_segment: None,
merge_layers: helper_meta.and_then(|meta| meta.merge_layers.clone()),
range_key: false,
digest: false,
merge_operand: self.merge_operand_paths.contains(path),
omitted_members: helper_meta
.map(|meta| meta.omitted_keys.clone())
.unwrap_or_default(),
provenance: helper_meta
.map(|meta| meta.provenance.clone())
.unwrap_or_default(),
site: None,
},
}
}
pub(super) fn path_splice_arms(
&self,
path: &str,
kind: ValueKind,
) -> Vec<(PathCondition, Splice)> {
let meta = self.local_output_meta.get(path);
match meta {
Some(meta) if !meta.predicates.is_empty() => helper_meta_conditions(meta)
.into_iter()
.map(|condition| (condition, self.splice(path, kind, Some(meta))))
.collect(),
_ => vec![(Predicate::True, self.splice(path, kind, meta))],
}
}
}
fn helper_meta_conditions(meta: &HelperOutputMeta) -> Vec<PathCondition> {
if meta.predicates.is_empty() {
return vec![Predicate::True];
}
meta.predicates
.iter()
.map(|branch| Predicate::all(branch.iter().cloned().collect()))
.collect()
}
fn flattened_merge_layers(layers: &[AbstractValue]) -> Vec<&AbstractValue> {
let mut flat = Vec::new();
for layer in layers {
match layer {
AbstractValue::MergedLayers(inner) => flat.extend(flattened_merge_layers(inner)),
other => flat.push(other),
}
}
flat
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(crate) fn lower_value(
value: &AbstractValue,
kind: ValueKind,
scope: &LowerScope<'_>,
) -> Guarded<AbstractFragment> {
match value {
AbstractValue::Top
| AbstractValue::Unknown
| AbstractValue::KeysList(_)
| AbstractValue::RootContext => {
Guarded::unconditional(AbstractFragment::Opaque(Opaque::default()))
}
AbstractValue::RangeKey(path) => {
if path.is_empty() {
Guarded::unconditional(AbstractFragment::Opaque(Opaque::default()))
} else {
let mut splice = scope.splice(path, kind, None);
splice.meta.range_key = true;
Guarded::unconditional(AbstractFragment::Splice(splice))
}
}
AbstractValue::ValuesPath(path) => {
if path.is_empty() {
Guarded::unconditional(AbstractFragment::Opaque(Opaque::default()))
} else {
let mut out = Guarded::empty();
for (condition, splice) in scope.path_splice_arms(path, kind) {
out.arms.push((condition, AbstractFragment::Splice(splice)));
}
out
}
}
AbstractValue::JsonDecodedPath(path) => {
if path.is_empty() {
Guarded::unconditional(AbstractFragment::Opaque(Opaque::default()))
} else {
let mut out = Guarded::empty();
for (condition, mut splice) in scope.path_splice_arms(path, kind) {
splice.meta.json_decoded = true;
out.arms.push((condition, AbstractFragment::Splice(splice)));
}
out
}
}
AbstractValue::OutputPath(path, meta) => {
let meta = scope
.local_source_paths
.contains(path)
.then(|| scope.local_output_meta.get(path))
.flatten()
.unwrap_or(meta);
let kind = if kind == ValueKind::Scalar && meta.partial_text {
ValueKind::PartialScalar
} else {
kind
};
let mut out = Guarded::empty();
for condition in helper_meta_conditions(meta) {
out.arms.push((
condition,
AbstractFragment::Splice(scope.splice(path, kind, Some(meta))),
));
}
out
}
AbstractValue::StringSet(strings) => {
Guarded::unconditional(AbstractFragment::Scalar(AbstractString {
parts: vec![StringPart::Text(strings.clone())],
suppressed: false,
}))
}
AbstractValue::DerivedBoolean(paths) => {
Guarded::unconditional(AbstractFragment::Opaque(Opaque {
taint: paths.clone(),
kind: ValueKind::Serialized,
..Opaque::default()
}))
}
AbstractValue::Dict(entries) => json_serialized_scalar(value, scope).unwrap_or_else(|| {
Guarded::unconditional(AbstractFragment::Mapping(lower_entries(entries, scope)))
}),
AbstractValue::List(items) => {
if let Some(serialized) = json_serialized_scalar(value, scope) {
return serialized;
}
let items = items
.iter()
.map(|item| lower_value(item, structure_child_kind(item), scope))
.collect();
Guarded::unconditional(AbstractFragment::Sequence(Sequence { items }))
}
AbstractValue::Overlay { entries, fallback } => {
if let Some(serialized) = json_serialized_scalar(value, scope) {
return serialized;
}
let mut out =
Guarded::unconditional(AbstractFragment::Mapping(lower_entries(entries, scope)));
out.extend(lower_value(fallback, kind, scope));
out
}
AbstractValue::Choice(choices) => {
let mut strings = BTreeSet::new();
let mut out = Guarded::empty();
for choice in choices {
if let AbstractValue::StringSet(members) = choice {
strings.extend(members.iter().cloned());
} else {
out.extend(lower_value(choice, kind, scope));
}
}
if !strings.is_empty() {
out.arms.push((
Predicate::True,
AbstractFragment::Scalar(AbstractString {
parts: vec![StringPart::Text(strings)],
suppressed: false,
}),
));
}
out
}
AbstractValue::FirstTruthy(candidates) => {
let mut strings = BTreeSet::new();
let mut out = Guarded::empty();
for candidate in candidates {
if let AbstractValue::StringSet(members) = candidate {
strings.extend(members.iter().cloned());
} else {
out.extend(lower_value(candidate, kind, scope));
}
}
if !strings.is_empty() {
out.arms.push((
Predicate::True,
AbstractFragment::Scalar(AbstractString {
parts: vec![StringPart::Text(strings)],
suppressed: false,
}),
));
}
out
}
AbstractValue::MergedLayers(layers) => {
let layers = flattened_merge_layers(layers);
let identities: Option<Vec<String>> = layers
.iter()
.map(|layer| layer.merge_layer_identity())
.collect();
let mut out = Guarded::empty();
for (position, layer) in layers.iter().enumerate() {
let mut lowered = lower_value(layer, kind, scope);
if let Some(layer_paths) = &identities {
for (_, fragment) in &mut lowered.arms {
if let AbstractFragment::Splice(splice) = fragment
&& layer_paths
.get(position)
.is_some_and(|path| splice.values_path == *path)
{
splice.meta.merge_layers = Some(helm_schema_core::MergeLayersUse {
layers: layer_paths.clone(),
position,
nil_scrubbed_layers: layers
.iter()
.map(|layer| {
matches!(
layer,
AbstractValue::OutputPath(_, meta)
if meta.nil_scrubbed
)
})
.collect(),
via_binding: false,
});
}
}
}
out.extend(lowered);
}
out
}
AbstractValue::SplitSegment {
source_paths,
separator,
last,
total_text_preimage: false,
} if source_paths.len() == 1 && source_paths.iter().all(|path| !path.is_empty()) => {
let mut out = Guarded::empty();
for path in source_paths {
for (condition, mut splice) in scope.path_splice_arms(path, kind) {
splice.meta.split_segment = Some(helm_schema_core::SplitSegmentUse {
separator: separator.clone(),
last: *last,
});
out.arms.push((condition, AbstractFragment::Splice(splice)));
}
}
out
}
AbstractValue::Widened(paths)
| AbstractValue::SplitList {
source_paths: paths,
..
}
| AbstractValue::SplitSegment {
source_paths: paths,
..
} => {
let mut out = Guarded::empty();
let mut taint = BTreeSet::new();
for path in paths {
match scope.local_output_meta.get(path) {
Some(meta) if !meta.predicates.is_empty() || meta.defaulted => {
let digest_input =
matches!(kind, ValueKind::Scalar | ValueKind::PartialScalar)
&& path_is_encoded(path, scope.derived_text_paths)
&& !path_is_encoded(path, scope.shape_erased_paths)
&& !path_is_encoded(path, scope.encoded_paths);
for (condition, mut splice) in scope.path_splice_arms(path, kind) {
splice.meta.digest |= digest_input;
out.arms.push((condition, AbstractFragment::Splice(splice)));
}
}
_ => {
taint.insert(path.clone());
}
}
}
if !taint.is_empty() {
let kind = if taint.iter().all(|path| {
path_is_encoded(path, scope.shape_erased_paths)
|| path_is_encoded(path, scope.derived_text_paths)
}) {
ValueKind::Serialized
} else {
kind
};
out.arms.push((
Predicate::True,
AbstractFragment::Opaque(Opaque {
taint,
kind,
..Opaque::default()
}),
));
}
out
}
}
}
fn lower_entries(
entries: &std::collections::BTreeMap<String, AbstractValue>,
scope: &LowerScope<'_>,
) -> Mapping {
Mapping {
entries: entries
.iter()
.map(|(key, value)| MappingEntry {
key: EntryKey::Literal(key.clone()),
value: lower_value(value, structure_child_kind(value), scope),
})
.collect(),
}
}
fn structure_child_kind(value: &AbstractValue) -> ValueKind {
match value {
AbstractValue::Dict(_) | AbstractValue::List(_) | AbstractValue::Overlay { .. } => {
ValueKind::Fragment
}
AbstractValue::Choice(choices)
if choices.iter().any(|choice| {
matches!(
choice,
AbstractValue::Dict(_) | AbstractValue::List(_) | AbstractValue::Overlay { .. }
)
}) =>
{
ValueKind::Fragment
}
_ => ValueKind::Scalar,
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
pub(crate) fn lower_value_scalar_arms(
value: &AbstractValue,
kind: ValueKind,
scope: &LowerScope<'_>,
) -> Vec<(PathCondition, Vec<StringPart>)> {
match value {
AbstractValue::Top
| AbstractValue::Unknown
| AbstractValue::KeysList(_)
| AbstractValue::RootContext => Vec::new(),
AbstractValue::RangeKey(path) => {
if path.is_empty() {
return Vec::new();
}
let mut splice = scope.splice(path, kind, None);
splice.meta.range_key = true;
vec![(Predicate::True, vec![StringPart::Splice(splice)])]
}
AbstractValue::ValuesPath(path) => {
if path.is_empty() {
Vec::new()
} else {
scope
.path_splice_arms(path, kind)
.into_iter()
.map(|(condition, splice)| (condition, vec![StringPart::Splice(splice)]))
.collect()
}
}
AbstractValue::JsonDecodedPath(path) => {
if path.is_empty() {
Vec::new()
} else {
scope
.path_splice_arms(path, kind)
.into_iter()
.map(|(condition, mut splice)| {
splice.meta.json_decoded = true;
(condition, vec![StringPart::Splice(splice)])
})
.collect()
}
}
AbstractValue::OutputPath(path, meta) => {
let meta = scope
.local_source_paths
.contains(path)
.then(|| scope.local_output_meta.get(path))
.flatten()
.unwrap_or(meta);
helper_meta_conditions(meta)
.into_iter()
.map(|condition| {
(
condition,
vec![StringPart::Splice(scope.splice(path, kind, Some(meta)))],
)
})
.collect()
}
AbstractValue::StringSet(strings) => {
vec![(Predicate::True, vec![StringPart::Text(strings.clone())])]
}
AbstractValue::DerivedBoolean(paths) => vec![(
Predicate::True,
vec![StringPart::Taint(TaintPart::new(paths.clone()))],
)],
AbstractValue::Dict(_)
| AbstractValue::List(_)
| AbstractValue::Overlay { .. }
| AbstractValue::MergedLayers(_) => {
let taint = json_serialized_taint(value, scope)
.unwrap_or_else(|| TaintPart::new(value.fragment_rendered_paths()));
vec![(Predicate::True, vec![StringPart::Taint(taint)])]
}
AbstractValue::Choice(choices) => {
lower_alternative_scalar_arms(choices.iter(), kind, scope)
}
AbstractValue::FirstTruthy(candidates) => {
lower_alternative_scalar_arms(candidates.iter(), kind, scope)
}
AbstractValue::Widened(paths)
| AbstractValue::SplitList {
source_paths: paths,
..
}
| AbstractValue::SplitSegment {
source_paths: paths,
..
} => {
let mut arms = Vec::new();
let mut taint = BTreeSet::new();
for path in paths {
match scope.local_output_meta.get(path) {
Some(meta) if !meta.predicates.is_empty() || meta.defaulted => {
for (condition, splice) in scope.path_splice_arms(path, kind) {
arms.push((condition, vec![StringPart::Splice(splice)]));
}
}
_ => {
taint.insert(path.clone());
}
}
}
if !taint.is_empty() {
arms.push((
Predicate::True,
vec![StringPart::Taint(TaintPart::new(taint))],
));
}
arms
}
}
}
fn lower_alternative_scalar_arms<'v>(
alternatives: impl Iterator<Item = &'v AbstractValue>,
kind: ValueKind,
scope: &LowerScope<'_>,
) -> Vec<(PathCondition, Vec<StringPart>)> {
let mut base_parts = Vec::new();
let mut conditional_arms = Vec::new();
for alternative in alternatives {
for (condition, parts) in lower_value_scalar_arms(alternative, kind, scope) {
if condition == Predicate::True {
base_parts.extend(parts);
} else {
conditional_arms.push((condition, parts));
}
}
}
let mut arms = Vec::new();
if !base_parts.is_empty() || conditional_arms.is_empty() {
arms.push((Predicate::True, base_parts));
}
arms.extend(conditional_arms);
if arms.len() > MAX_SCALAR_ARM_FANOUT {
let parts = arms.into_iter().flat_map(|(_, parts)| parts).collect();
return vec![(Predicate::True, parts)];
}
arms
}
fn json_serialized_taint(value: &AbstractValue, scope: &LowerScope<'_>) -> Option<TaintPart> {
let paths = value.paths();
if paths.is_empty()
|| !paths
.iter()
.all(|path| path_is_encoded(path, scope.json_serialized_paths))
{
return None;
}
let root_structure = value.values_root_structure()?;
Some(TaintPart::from_json_serialized(root_structure))
}
fn json_serialized_scalar(
value: &AbstractValue,
scope: &LowerScope<'_>,
) -> Option<Guarded<AbstractFragment>> {
let taint = json_serialized_taint(value, scope)?;
Some(Guarded::unconditional(AbstractFragment::Scalar(
AbstractString {
parts: vec![StringPart::Taint(taint)],
suppressed: false,
},
)))
}