use std::collections::{BTreeMap, HashMap};
use helm_schema_core::{
ContractRequirementImplication, ContractRequirementTarget, ContractSchemaSignals,
FailValueRequirement, ProviderSchemaUse, ResourceSchemaOracle, ValueKind,
};
use serde_json::Value;
use serde_yaml::Value as YamlValue;
#[expect(
clippy::too_many_lines,
reason = "the direct and conditional projections share one provider-nullability cache and source-default invariant"
)]
pub(crate) fn synthesized_required_source_implications(
contract_schema_signals: &ContractSchemaSignals,
values_yaml_doc: &YamlValue,
subchart_defaults_doc: &YamlValue,
provider: &dyn ResourceSchemaOracle,
) -> BTreeMap<String, Vec<ContractRequirementImplication>> {
let mut implications: BTreeMap<String, Vec<ContractRequirementImplication>> = BTreeMap::new();
let mut null_rejected_by_use: HashMap<ProviderSchemaUse, bool> = HashMap::new();
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
let segments = crate::split_value_path(value_path);
let Some((leaf_segment, parent_segments)) = segments.split_last() else {
continue;
};
if segments.iter().any(|segment| segment == "*") {
continue;
}
let base_facts = evidence.facts;
if base_facts.has_unconditional_render_use
&& !source_has_dependency_default(subchart_defaults_doc, value_path)
&& source_has_non_null_default(values_yaml_doc, value_path)
&& !base_facts.is_nullable
&& evidence.provider_schema_uses.iter().any(|use_| {
use_.kind == ValueKind::Scalar
&& !use_.is_self_range_collection
&& !use_.source_null_tolerant
&& use_.merge_layers.is_none()
&& !use_.range_key
&& use_.outer_guards.is_empty()
&& *null_rejected_by_use
.entry(use_.clone())
.or_insert_with(|| provider_use_rejects_null(provider, use_))
})
{
push_implication(
&mut implications,
helm_schema_core::join_value_path(parent_segments.iter().cloned()),
ContractRequirementImplication {
outer_guards: Vec::new(),
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::HasMemberEvenDefaulted(
leaf_segment.clone(),
)],
},
);
push_implication(
&mut implications,
value_path.clone(),
ContractRequirementImplication {
outer_guards: Vec::new(),
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::NotSchemaType("null".to_string())],
},
);
}
for overlay in &evidence.conditional_overlays {
if overlay.guards.is_empty() {
continue;
}
if source_has_dependency_default(subchart_defaults_doc, value_path) {
continue;
}
if !source_has_non_null_default(values_yaml_doc, value_path)
&& crate::condition_encoding::evaluate_guard_set_on_values(
&overlay.guards,
values_yaml_doc,
) != Some(false)
{
continue;
}
if overlay
.guards
.iter()
.any(|guard| guard.value_paths().contains(value_path))
{
continue;
}
let facts = overlay.evidence.facts;
if facts.is_nullable {
continue;
}
let renders_into_required_field =
overlay.evidence.provider_schema_uses.iter().any(|use_| {
use_.kind == ValueKind::Scalar
&& !use_.is_self_range_collection
&& !use_.source_null_tolerant
&& use_.merge_layers.is_none()
&& !use_.range_key
&& *null_rejected_by_use
.entry(use_.clone())
.or_insert_with(|| provider_use_rejects_null(provider, use_))
});
if !renders_into_required_field {
continue;
}
push_implication(
&mut implications,
helm_schema_core::join_value_path(parent_segments.iter().cloned()),
ContractRequirementImplication {
outer_guards: overlay.guards.clone(),
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::HasMemberEvenDefaulted(
leaf_segment.clone(),
)],
},
);
push_implication(
&mut implications,
value_path.clone(),
ContractRequirementImplication {
outer_guards: overlay.guards.clone(),
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::NotSchemaType("null".to_string())],
},
);
}
}
implications
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
)]
pub(crate) fn synthesized_ranged_member_required_implications(
contract_schema_signals: &ContractSchemaSignals,
subchart_defaults_doc: &YamlValue,
provider: &dyn ResourceSchemaOracle,
) -> BTreeMap<String, Vec<ContractRequirementImplication>> {
let mut implications: BTreeMap<String, Vec<ContractRequirementImplication>> = BTreeMap::new();
let mut null_rejected_by_use: HashMap<ProviderSchemaUse, bool> = HashMap::new();
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
let segments = crate::split_value_path(value_path);
let Some(star) = segments.iter().position(|segment| segment == "*") else {
continue;
};
let Some(collection_segments) = segments.get(..star) else {
continue;
};
let Some(field_segments) = segments.get(star + 1..) else {
continue;
};
if collection_segments.is_empty()
|| field_segments.is_empty()
|| field_segments.iter().any(|segment| segment == "*")
{
continue;
}
let collection_path =
helm_schema_core::join_value_path(collection_segments.iter().cloned());
let member_scope = format!("{collection_path}.*");
let base_uses = std::iter::once((
&[] as &[helm_schema_core::ConditionalGuard],
evidence.facts,
&evidence.provider_schema_uses,
));
let overlay_uses = evidence.conditional_overlays.iter().map(|overlay| {
(
overlay.guards.as_slice(),
overlay.evidence.facts,
&overlay.evidence.provider_schema_uses,
)
});
for (guards, facts, uses) in base_uses.chain(overlay_uses) {
if facts.is_nullable {
continue;
}
let renders_into_required_field = uses.iter().any(|use_| {
let null_forcing = match use_.kind {
ValueKind::Scalar => true,
ValueKind::Serialized => use_.nil_omitting,
_ => false,
};
null_forcing
&& !use_.is_self_range_collection
&& !use_.source_null_tolerant
&& use_.merge_layers.is_none()
&& !use_.range_key
&& use_.split_segment.is_none()
&& *null_rejected_by_use
.entry(use_.clone())
.or_insert_with(|| provider_use_rejects_null(provider, use_))
});
if !renders_into_required_field {
continue;
}
let mut outer_guards = Vec::new();
let mut escapes: Vec<Vec<FailValueRequirement>> = Vec::new();
let mut undecodable = false;
for guard in guards {
let paths = guard.value_paths();
if paths.iter().all(|path| !path.contains('*')) {
outer_guards.push(guard.clone());
continue;
}
let member_field = |path: &str| {
path.strip_prefix(&format!("{member_scope}."))
.filter(|field| !field.contains('*'))
.map(helm_schema_core::split_value_path)
};
match guard {
helm_schema_core::ConditionalGuard::Not(inner) => match inner.as_ref() {
helm_schema_core::ConditionalGuard::Truthy { path } => {
match member_field(path) {
Some(field) => {
escapes.push(vec![FailValueRequirement::FieldHelmTruthy {
path: field,
}]);
}
None => undecodable = true,
}
}
_ => undecodable = true,
},
helm_schema_core::ConditionalGuard::Truthy { path }
if path == &member_scope =>
{
escapes.push(vec![FailValueRequirement::HelmFalsy]);
}
helm_schema_core::ConditionalGuard::Truthy { path } => {
match member_field(path) {
Some(field) => {
escapes.push(vec![FailValueRequirement::FieldHelmFalsy {
path: field,
}]);
}
None => undecodable = true,
}
}
_ => undecodable = true,
}
}
if undecodable {
continue;
}
let field_path: Vec<String> = field_segments.to_vec();
let presence = FailValueRequirement::FieldPresentNotNull { path: field_path };
let requirements = if escapes.is_empty() {
vec![presence]
} else {
let mut alternatives = escapes;
alternatives.push(vec![presence]);
vec![FailValueRequirement::AnyOf(alternatives)]
};
push_implication(
&mut implications,
collection_path.clone(),
ContractRequirementImplication {
outer_guards,
target: dependency_defaulted_member_keys(
subchart_defaults_doc,
collection_segments,
field_segments,
)
.map_or(
ContractRequirementTarget::Members {
allow_integer: true,
},
|keys| ContractRequirementTarget::MembersExceptKeys {
keys,
allow_integer: true,
},
),
requirements,
},
);
}
}
implications
}
fn source_has_non_null_default(values_yaml_doc: &YamlValue, value_path: &str) -> bool {
crate::values_yaml::yaml_value_at_path(values_yaml_doc, value_path)
.is_some_and(|value| !value.is_null())
}
fn source_has_dependency_default(subchart_defaults_doc: &YamlValue, value_path: &str) -> bool {
crate::values_yaml::yaml_value_at_path(subchart_defaults_doc, value_path).is_some()
}
fn dependency_defaulted_member_keys(
subchart_defaults_doc: &YamlValue,
collection_segments: &[String],
field_segments: &[String],
) -> Option<std::collections::BTreeSet<String>> {
let YamlValue::Mapping(members) =
crate::values_yaml::yaml_value_at_segments(subchart_defaults_doc, collection_segments)?
else {
return None;
};
let keys = members
.iter()
.filter_map(|(key, member)| {
let key = key.as_str()?;
crate::values_yaml::yaml_value_at_segments(member, field_segments)
.filter(|default| !default.is_null())
.map(|_| key.to_string())
})
.collect::<std::collections::BTreeSet<_>>();
(!keys.is_empty()).then_some(keys)
}
fn provider_use_rejects_null(
provider: &dyn ResourceSchemaOracle,
provider_use: &ProviderSchemaUse,
) -> bool {
let Some(fragment) = provider.schema_fragment_for_use(provider_use) else {
return false;
};
jsonschema::validator_for(fragment.schema())
.is_ok_and(|validator| !validator.is_valid(&Value::Null))
}
pub(crate) fn synthesized_split_segment_implications(
contract_schema_signals: &ContractSchemaSignals,
provider: &dyn ResourceSchemaOracle,
) -> BTreeMap<String, Vec<ContractRequirementImplication>> {
let mut implications: BTreeMap<String, Vec<ContractRequirementImplication>> = BTreeMap::new();
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
for use_ in &evidence.provider_schema_uses {
let Some(segment) = &use_.split_segment else {
continue;
};
if use_.kind != ValueKind::Scalar {
continue;
}
let Some(pattern) = provider.schema_fragment_for_use(use_).and_then(|fragment| {
crate::resolve_policy::split_segment_pattern(fragment.schema(), segment)
}) else {
continue;
};
push_implication(
&mut implications,
value_path.clone(),
ContractRequirementImplication {
outer_guards: vec![helm_schema_core::ConditionalGuard::Truthy {
path: value_path.clone(),
}],
target: ContractRequirementTarget::Value,
requirements: vec![FailValueRequirement::MatchesPattern {
pattern,
templated: false,
}],
},
);
}
}
implications
}
pub(crate) fn synthesized_range_key_implications(
contract_schema_signals: &ContractSchemaSignals,
provider: &dyn ResourceSchemaOracle,
) -> BTreeMap<String, Vec<ContractRequirementImplication>> {
let mut implications: BTreeMap<String, Vec<ContractRequirementImplication>> = BTreeMap::new();
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
let overlay_uses = evidence.conditional_overlays.iter().flat_map(|overlay| {
overlay
.evidence
.provider_schema_uses
.iter()
.map(move |use_| (overlay.guards.as_slice(), use_))
});
for (branch_guards, use_) in evidence
.provider_schema_uses
.iter()
.map(|use_| (&[] as &[helm_schema_core::ConditionalGuard], use_))
.chain(overlay_uses)
{
if !use_.range_key || use_.kind != ValueKind::Scalar {
continue;
}
let Some(fragment) = provider.schema_fragment_for_use(use_) else {
continue;
};
if crate::overlay_lowering::schema_runtime_types(fragment.schema())
!= std::collections::BTreeSet::from(["string"])
{
continue;
}
let mut requirements = vec![FailValueRequirement::SchemaType("string".to_string())];
if let Some(pattern) = fragment.schema().get("pattern").and_then(Value::as_str) {
requirements.push(FailValueRequirement::MatchesPattern {
pattern: pattern.to_string(),
templated: false,
});
}
let min = fragment.schema().get("minLength").and_then(Value::as_u64);
let max = fragment.schema().get("maxLength").and_then(Value::as_u64);
if min.is_some() || max.is_some() {
requirements.push(FailValueRequirement::StringLengthBounds { min, max });
}
push_implication(
&mut implications,
value_path.clone(),
ContractRequirementImplication {
outer_guards: branch_guards.to_vec(),
target: ContractRequirementTarget::Keys,
requirements,
},
);
}
}
implications
}
fn push_implication(
implications: &mut BTreeMap<String, Vec<ContractRequirementImplication>>,
target_value_path: String,
implication: ContractRequirementImplication,
) {
let entries = implications.entry(target_value_path).or_default();
if !entries.contains(&implication) {
entries.push(implication);
}
}