use std::collections::BTreeSet;
use crate::abstract_value::AbstractValue;
pub(super) fn value_paths(value: Option<&AbstractValue>) -> BTreeSet<String> {
value.map(AbstractValue::paths).unwrap_or_default()
}
pub(super) fn value_strings(value: Option<&AbstractValue>) -> BTreeSet<String> {
value.map(AbstractValue::strings).unwrap_or_default()
}
pub(super) fn identity_value_paths(value: Option<&AbstractValue>) -> BTreeSet<String> {
fn collect(value: &AbstractValue, paths: &mut BTreeSet<String>) {
match value {
AbstractValue::ValuesPath(path)
| AbstractValue::JsonDecodedPath(path)
| AbstractValue::OutputPath(path, _) => {
paths.insert(path.clone());
}
AbstractValue::Choice(choices) => {
for choice in choices {
collect(choice, paths);
}
}
AbstractValue::FirstTruthy(candidates) => {
for candidate in candidates {
collect(candidate, paths);
}
}
AbstractValue::MergedLayers(layers) => {
for layer in layers {
collect(layer, paths);
}
}
AbstractValue::Top
| AbstractValue::Unknown
| AbstractValue::RangeKey(_)
| AbstractValue::KeysList(_)
| AbstractValue::RootContext
| AbstractValue::StringSet(_)
| AbstractValue::DerivedBoolean(_)
| AbstractValue::Dict(_)
| AbstractValue::List(_)
| AbstractValue::Overlay { .. }
| AbstractValue::SplitList { .. }
| AbstractValue::SplitSegment { .. }
| AbstractValue::Widened(_) => {}
}
}
let mut paths = BTreeSet::new();
if let Some(value) = value {
collect(value, &mut paths);
}
paths
}
pub(super) fn serialization_payload_paths(value: Option<&AbstractValue>) -> BTreeSet<String> {
fn collect(value: &AbstractValue, paths: &mut BTreeSet<String>) {
match value {
AbstractValue::ValuesPath(path)
| AbstractValue::JsonDecodedPath(path)
| AbstractValue::OutputPath(path, _) => {
paths.insert(path.clone());
}
AbstractValue::Dict(entries) => {
for value in entries.values() {
collect(value, paths);
}
}
AbstractValue::List(items) => {
for value in items {
collect(value, paths);
}
}
AbstractValue::Overlay { entries, fallback } => {
for value in entries.values() {
collect(value, paths);
}
collect(fallback, paths);
}
AbstractValue::Choice(choices) => {
for value in choices {
collect(value, paths);
}
}
AbstractValue::FirstTruthy(candidates) => {
for value in candidates {
collect(value, paths);
}
}
AbstractValue::MergedLayers(layers) => {
for value in layers {
collect(value, paths);
}
}
AbstractValue::Top
| AbstractValue::Unknown
| AbstractValue::RangeKey(_)
| AbstractValue::KeysList(_)
| AbstractValue::RootContext
| AbstractValue::StringSet(_)
| AbstractValue::DerivedBoolean(_)
| AbstractValue::SplitList { .. }
| AbstractValue::SplitSegment { .. }
| AbstractValue::Widened(_) => {}
}
}
let mut paths = BTreeSet::new();
if let Some(value) = value {
collect(value, &mut paths);
}
paths
}
pub(super) fn identity_range_key_paths(value: Option<&AbstractValue>) -> BTreeSet<String> {
value
.map(AbstractValue::range_key_paths)
.unwrap_or_default()
}
pub(super) fn concrete_collection_len(value: &AbstractValue) -> Option<usize> {
match value {
AbstractValue::List(items) => Some(items.len()),
AbstractValue::Dict(entries) => Some(entries.len()),
_ => None,
}
}
pub(super) fn concrete_integer(value: &AbstractValue) -> Option<i64> {
let AbstractValue::StringSet(strings) = value else {
return None;
};
let mut strings = strings.iter();
let (Some(text), None) = (strings.next(), strings.next()) else {
return None;
};
text.parse().ok()
}
pub(super) fn escape_wrapped_identity(
value: &AbstractValue,
effects: &crate::eval_effect::Effects,
escape: crate::helper_meta::LexicalEscape,
) -> Option<AbstractValue> {
match value {
AbstractValue::ValuesPath(path) => {
if effects.shape_erased_paths.contains(path)
|| effects.derived_text_paths.contains(path)
|| effects
.local_output_meta
.get(path)
.is_some_and(|meta| meta.shape_erased || meta.derived_text)
{
return None;
}
let mut meta = crate::helper_meta::HelperOutputMeta::default();
meta.lexical_escapes.insert(escape);
Some(AbstractValue::OutputPath(path.clone(), meta))
}
AbstractValue::OutputPath(path, meta) => {
if meta.shape_erased
|| meta.derived_text
|| meta.yaml_serialized
|| meta.json_serialized
{
return None;
}
let mut meta = meta.clone();
meta.lexical_escapes.insert(escape);
Some(AbstractValue::OutputPath(path.clone(), meta))
}
_ => None,
}
}
pub(super) fn replace_transformed_value(
value: &AbstractValue,
effects: &crate::eval_effect::Effects,
old: &str,
new_values: &BTreeSet<String>,
) -> Option<AbstractValue> {
match value {
AbstractValue::StringSet(strings) => {
if new_values.is_empty() {
return None;
}
let mut rendered = BTreeSet::new();
for subject in strings {
for new in new_values {
rendered.insert(subject.replace(old, new));
}
}
Some(AbstractValue::StringSet(rendered))
}
AbstractValue::Choice(choices) => {
let mapped = choices
.iter()
.map(|choice| replace_transformed_value(choice, effects, old, new_values))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
AbstractValue::FirstTruthy(candidates) => {
let mapped = candidates
.iter()
.map(|candidate| replace_transformed_value(candidate, effects, old, new_values))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
other => escape_wrapped_identity(
other,
effects,
crate::helper_meta::LexicalEscape::Contains(old.to_string()),
),
}
}
pub(super) fn split_transformed_value(
value: &AbstractValue,
effects: &crate::eval_effect::Effects,
separator: &str,
) -> Option<AbstractValue> {
match value {
AbstractValue::StringSet(strings) => {
let members = strings
.iter()
.map(|text| {
AbstractValue::Dict(
text.split(separator)
.enumerate()
.map(|(index, part)| {
(
format!("_{index}"),
AbstractValue::StringSet(
[part.to_string()].into_iter().collect(),
),
)
})
.collect(),
)
})
.collect();
AbstractValue::choice(members)
}
AbstractValue::Choice(choices) => {
let mapped = choices
.iter()
.map(|choice| split_transformed_value(choice, effects, separator))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
AbstractValue::FirstTruthy(candidates) => {
let mapped = candidates
.iter()
.map(|candidate| split_transformed_value(candidate, effects, separator))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
other => {
let prefix = escape_wrapped_identity(
other,
effects,
crate::helper_meta::LexicalEscape::Contains(separator.to_string()),
)?;
Some(AbstractValue::Overlay {
entries: std::collections::BTreeMap::from([("_0".to_string(), prefix)]),
fallback: Box::new(AbstractValue::Unknown),
})
}
}
}
pub(super) fn mark_stringified_identities(value: Option<AbstractValue>) -> Option<AbstractValue> {
fn mark(value: AbstractValue) -> AbstractValue {
match value {
AbstractValue::OutputPath(path, mut meta) => {
if !meta.derived_text
&& !meta.shape_erased
&& !meta.yaml_serialized
&& !meta.json_serialized
{
meta.stringified = true;
}
AbstractValue::OutputPath(path, meta)
}
AbstractValue::Choice(choices) => {
AbstractValue::Choice(choices.into_iter().map(mark).collect())
}
AbstractValue::FirstTruthy(candidates) => {
AbstractValue::FirstTruthy(candidates.into_iter().map(mark).collect())
}
other => other,
}
}
value.map(mark)
}
pub(super) fn derive_value_text(value: Option<AbstractValue>) -> Option<AbstractValue> {
fn mark(value: AbstractValue) -> AbstractValue {
match value {
AbstractValue::OutputPath(path, mut meta) => {
meta.derived_text = true;
AbstractValue::OutputPath(path, meta)
}
AbstractValue::Choice(choices) => {
AbstractValue::Choice(choices.into_iter().map(mark).collect())
}
AbstractValue::FirstTruthy(candidates) => {
AbstractValue::FirstTruthy(candidates.into_iter().map(mark).collect())
}
other => other,
}
}
value.map(mark)
}
pub(super) fn trim_affix_transformed_value(
value: &AbstractValue,
effects: &crate::eval_effect::Effects,
token: &str,
prefix: bool,
) -> Option<AbstractValue> {
match value {
AbstractValue::StringSet(strings) => Some(AbstractValue::StringSet(
strings
.iter()
.map(|text| {
let trimmed = if prefix {
text.strip_prefix(token)
} else {
text.strip_suffix(token)
};
trimmed.unwrap_or(text).to_string()
})
.collect(),
)),
AbstractValue::Choice(choices) => {
let mapped = choices
.iter()
.map(|choice| trim_affix_transformed_value(choice, effects, token, prefix))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
AbstractValue::FirstTruthy(candidates) => {
let mapped = candidates
.iter()
.map(|candidate| trim_affix_transformed_value(candidate, effects, token, prefix))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
other => {
let escape = if prefix {
crate::helper_meta::LexicalEscape::TrimPrefix(token.to_string())
} else {
crate::helper_meta::LexicalEscape::TrimSuffix(token.to_string())
};
escape_wrapped_identity(other, effects, escape)
}
}
}
pub(super) fn regex_replace_transformed_value(
value: &AbstractValue,
effects: &crate::eval_effect::Effects,
escape: &crate::helper_meta::LexicalEscape,
) -> Option<AbstractValue> {
match value {
AbstractValue::Choice(choices) => {
let mapped = choices
.iter()
.map(|choice| regex_replace_transformed_value(choice, effects, escape))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
AbstractValue::FirstTruthy(candidates) => {
let mapped = candidates
.iter()
.map(|candidate| regex_replace_transformed_value(candidate, effects, escape))
.collect::<Option<Vec<_>>>()?;
AbstractValue::choice(mapped)
}
other => escape_wrapped_identity(other, effects, escape.clone()),
}
}
pub(super) fn regex_mandatory_literal(pattern: &str) -> Option<String> {
let pattern = pattern.strip_prefix('^').unwrap_or(pattern);
let mut literal = String::new();
let mut chars = pattern.chars().peekable();
while let Some(character) = chars.peek().copied() {
if matches!(
character,
'\\' | '.' | '^' | '$' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}'
) {
break;
}
literal.push(character);
chars.next();
}
if matches!(chars.peek(), Some('?' | '*' | '{')) {
literal.pop();
}
(!literal.is_empty()).then_some(literal)
}