use super::{
BTreeSet, ConditionalBaseEffect, ConditionalFlavor, ConditionalGuard, ConditionalPathOverlay,
ContractSchemaSignals, EmissionOrigin, LoweredConjunct, ProviderSchemaFragment,
ResolvedPathSchema, ResourceSchemaOracle, SchemaNode, Value, YamlValue,
};
pub(crate) fn member_descendant_projection(
target_segments: &[String],
descendants: &[&ResolvedPathSchema],
insertion_abstentions: &mut usize,
) -> Option<Value> {
let mut member_schema = descendants
.iter()
.filter_map(|descendant| {
let relative = descendant.path_segments.strip_prefix(target_segments)?;
(relative == ["*"]
&& !crate::schema_model::is_empty_schema(&descendant.structural_schema))
.then(|| descendant.structural_schema.clone())
})
.reduce(crate::merge::merge_two_schemas);
let mut has_descendant = member_schema.is_some();
for descendant in descendants {
let Some(relative) = descendant.path_segments.strip_prefix(target_segments) else {
continue;
};
let Some(("*", tail)) = relative
.split_first()
.map(|(head, tail)| (head.as_str(), tail))
else {
continue;
};
if tail.is_empty() {
continue;
}
has_descendant = true;
let current = member_schema
.take()
.unwrap_or_else(|| SchemaNode::untyped_member_host().into_value());
let (inserted, abstentions) = crate::schema_tree::insert_path_schema_value(
current,
tail,
descendant.structural_schema.clone(),
);
*insertion_abstentions += abstentions;
member_schema = Some(inserted);
}
if !has_descendant {
return None;
}
Some(member_schema.unwrap_or_else(|| SchemaNode::untyped_member_host().into_value()))
}
pub(super) fn structural_collection_member_projection(schema: &Value) -> Option<Value> {
if crate::schema_model::is_empty_schema(schema) {
return None;
}
let object = schema.as_object()?;
for keyword in ["anyOf", "oneOf"] {
let Some(arms) = object.get(keyword).and_then(Value::as_array) else {
continue;
};
let members = arms
.iter()
.filter_map(structural_collection_member_projection)
.collect::<Vec<_>>();
if members.is_empty() {
return None;
}
if members.iter().any(crate::schema_model::is_empty_schema) {
return Some(crate::schema_model::empty_schema());
}
return Some(
SchemaNode::any_of(members.into_iter().map(SchemaNode::foreign).collect()).into_value(),
);
}
if let Some(arms) = object.get("allOf").and_then(Value::as_array) {
let members = arms
.iter()
.filter_map(structural_collection_member_projection)
.filter(|member| !crate::schema_model::is_empty_schema(member))
.collect::<Vec<_>>();
if !members.is_empty() {
return Some(
SchemaNode::all_of(members.into_iter().map(SchemaNode::foreign).collect())
.into_value(),
);
}
}
let schema_types = match object.get("type") {
Some(Value::String(schema_type)) => vec![schema_type.as_str()],
Some(Value::Array(schema_types)) => schema_types
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>(),
_ => Vec::new(),
};
let object_lane = schema_types.contains(&"object")
|| (schema_types.is_empty()
&& object.keys().any(|keyword| {
matches!(
keyword.as_str(),
"additionalProperties"
| "maxProperties"
| "minProperties"
| "patternProperties"
| "properties"
| "propertyNames"
| "required"
)
}));
let array_lane = schema_types.contains(&"array")
|| (schema_types.is_empty()
&& object.keys().any(|keyword| {
matches!(
keyword.as_str(),
"additionalItems"
| "contains"
| "items"
| "maxItems"
| "minItems"
| "prefixItems"
| "uniqueItems"
)
}));
if !object_lane && !array_lane {
return None;
}
let mut members = Vec::new();
if object_lane {
let member = match object.get("additionalProperties") {
Some(Value::Bool(_)) | None => crate::schema_model::empty_schema(),
Some(schema) => schema.clone(),
};
members.push(member);
}
if array_lane {
let member = match object.get("items") {
Some(schema) if !schema.is_boolean() && !schema.is_array() => schema.clone(),
_ => crate::schema_model::empty_schema(),
};
members.push(member);
}
if members.iter().any(crate::schema_model::is_empty_schema) {
return Some(crate::schema_model::empty_schema());
}
match members.as_slice() {
[] => None,
[member] => Some(member.clone()),
_ => Some(
SchemaNode::any_of(members.into_iter().map(SchemaNode::foreign).collect()).into_value(),
),
}
}
pub(super) fn append_omitted_member_arms(
conditionals: &mut Vec<LoweredConjunct>,
contract_schema_signals: &ContractSchemaSignals,
provider: &dyn ResourceSchemaOracle,
) {
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
let mut arms: BTreeSet<(String, Vec<ConditionalGuard>, String)> = BTreeSet::new();
let uses_with_guards = evidence
.provider_schema_uses
.iter()
.map(|provider_use| (provider_use, Vec::new()))
.chain(evidence.conditional_overlays.iter().flat_map(|overlay| {
overlay
.evidence
.provider_schema_uses
.iter()
.map(|provider_use| (provider_use, overlay.guards.clone()))
}));
for (provider_use, branch_guards) in uses_with_guards {
if provider_use.omitted_members.is_empty() {
continue;
}
let Some(fragment) = provider.schema_fragment_for_use(provider_use) else {
continue;
};
let payload = fragment.schema();
let definitions = ["$defs", "definitions"]
.iter()
.find_map(|key| payload.get(*key).and_then(Value::as_object));
let Some(properties) = payload.get("properties").and_then(Value::as_object) else {
continue;
};
for (member, retain_guards) in &provider_use.omitted_members {
if retain_guards.is_empty() {
continue;
}
let Some(member_schema) = properties
.get(member)
.and_then(|schema| dereferenced_payload_subschema(schema, definitions, 8))
else {
continue;
};
let mut guards = branch_guards.clone();
guards.extend(retain_guards.iter().cloned());
guards.sort();
guards.dedup();
arms.insert((
member.clone(),
guards,
helm_schema_json_schema_walk::canonical_json_string(&member_schema),
));
}
}
let target_segments = value_path
.segments()
.map(helm_schema_core::Segment::encode_component)
.collect::<Vec<_>>();
for (member, guards, member_schema) in arms {
let Ok(member_schema) = serde_json::from_str::<Value>(&member_schema) else {
continue;
};
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::OmittedMember,
ConditionalFlavor::Ordinary,
value_path.clone(),
Vec::new(),
target_segments.clone(),
guards,
Vec::new(),
serde_json::json!({
"properties": { member: member_schema }
}),
None,
ConditionalBaseEffect::None,
false,
));
}
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
)]
pub(super) fn append_merge_shadow_arms(
conditionals: &mut Vec<LoweredConjunct>,
contract_schema_signals: &ContractSchemaSignals,
provider: &dyn ResourceSchemaOracle,
) {
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
for provider_use in &evidence.provider_schema_uses {
let Some(merge) = provider_use.merge_layers.as_ref() else {
continue;
};
let fragment = provider.schema_fragment_for_use(provider_use);
let payload = fragment.as_ref().map(ProviderSchemaFragment::schema);
let definitions = payload.and_then(|payload| {
["$defs", "definitions"]
.iter()
.find_map(|key| payload.get(*key).and_then(Value::as_object))
});
let target_segments = value_path
.segments()
.map(helm_schema_core::Segment::encode_component)
.collect::<Vec<_>>();
let provider_whole = payload
.and_then(Value::as_object)
.map(|object| Value::Object(object.clone()))
.and_then(|value| dereferenced_payload_subschema(&value, definitions, 8))
.map(|mut whole| {
if let Some(object) = whole.as_object_mut() {
object.remove("$defs");
object.remove("definitions");
}
whole
});
let metadata_whole = metadata_sink_schema(&provider_use.path.0);
let whole = match (provider_whole, metadata_whole) {
(Some(provider_whole), Some(metadata_whole)) => {
Some(crate::merge::merge_schema_list(vec![
provider_whole,
metadata_whole,
]))
}
(whole, None) | (None, whole) => whole,
};
if let Some(mut whole) = whole {
if merge.own_transform() == helm_schema_core::MergeLayerTransform::NilScrubbed {
null_relax_member_schemas(&mut whole);
}
let own_guard = match merge.own_transform() {
helm_schema_core::MergeLayerTransform::ParsedMap => ConditionalGuard::TypeIs {
path: value_path.clone(),
schema_type: "object".to_string(),
},
helm_schema_core::MergeLayerTransform::Identity
| helm_schema_core::MergeLayerTransform::NilScrubbed => {
ConditionalGuard::Truthy {
path: value_path.clone(),
}
}
};
let mut guards = vec![own_guard];
guards.extend(merge.shadowed_by().map(|earlier| {
let earlier_live = match earlier.transform {
helm_schema_core::MergeLayerTransform::ParsedMap => {
ConditionalGuard::AllOf(vec![
ConditionalGuard::TypeIs {
path: earlier.path.clone(),
schema_type: "object".to_string(),
},
ConditionalGuard::Truthy {
path: earlier.path.clone(),
},
])
}
helm_schema_core::MergeLayerTransform::Identity
| helm_schema_core::MergeLayerTransform::NilScrubbed => {
ConditionalGuard::Truthy {
path: earlier.path.clone(),
}
}
};
ConditionalGuard::Not(Box::new(earlier_live))
}));
guards.extend(provider_use.outer_guards.iter().cloned());
guards.sort();
guards.dedup();
let base_effect = if evidence.facts.has_unlayered_non_control_use {
ConditionalBaseEffect::Preserve
} else {
ConditionalBaseEffect::Own
};
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::MergeShadow,
ConditionalFlavor::Ordinary,
value_path.clone(),
Vec::new(),
target_segments.clone(),
guards,
Vec::new(),
whole,
None,
base_effect,
false,
));
}
if merge.position() == 0 {
continue;
}
let Some(properties) = payload
.and_then(|payload| payload.get("properties"))
.and_then(Value::as_object)
else {
continue;
};
for (member, member_schema) in properties {
let Some(mut member_schema) =
dereferenced_payload_subschema(member_schema, definitions, 8)
else {
continue;
};
if merge.own_transform() == helm_schema_core::MergeLayerTransform::NilScrubbed {
null_relax_member_schemas(&mut member_schema);
member_schema = serde_json::json!({
"anyOf": [member_schema, { "type": "null" }]
});
}
let mut guards: Vec<ConditionalGuard> = merge
.shadowed_by()
.map(|earlier| {
ConditionalGuard::Not(Box::new(ConditionalGuard::HasKey {
path: earlier.path.clone(),
key: member.clone(),
}))
})
.collect();
guards.extend(provider_use.outer_guards.iter().cloned());
guards.sort();
guards.dedup();
let target_schema = serde_json::json!({
"properties": { member: member_schema }
});
conditionals.push(LoweredConjunct::schema(
EmissionOrigin::MergeShadow,
ConditionalFlavor::Ordinary,
value_path.clone(),
Vec::new(),
target_segments.clone(),
guards,
Vec::new(),
target_schema,
None,
ConditionalBaseEffect::None,
false,
));
}
}
}
}
fn null_relax_member_schemas(schema: &mut Value) {
let Some(object) = schema.as_object_mut() else {
return;
};
for group in ["allOf", "anyOf", "oneOf"] {
if let Some(Value::Array(arms)) = object.get_mut(group) {
for arm in arms {
null_relax_member_schemas(arm);
}
}
}
for members_key in ["properties", "patternProperties"] {
if let Some(Value::Object(members)) = object.get_mut(members_key) {
for member in members.values_mut() {
null_relax_member_schemas(member);
if member.is_object() {
let original = std::mem::take(member);
*member = serde_json::json!({ "anyOf": [original, { "type": "null" }] });
}
}
}
}
if let Some(additional) = object.get_mut("additionalProperties")
&& additional.is_object()
{
null_relax_member_schemas(additional);
let original = std::mem::take(additional);
*additional = serde_json::json!({ "anyOf": [original, { "type": "null" }] });
}
}
fn metadata_sink_schema(path: &[String]) -> Option<Value> {
let parent = path
.len()
.checked_sub(2)
.and_then(|index| path.get(index))?;
if parent != "metadata" {
return None;
}
matches!(path.last()?.as_str(), "labels" | "annotations").then(|| {
serde_json::json!({
"type": "object",
"additionalProperties": { "type": "string" },
})
})
}
fn dereferenced_payload_subschema(
schema: &Value,
definitions: Option<&serde_json::Map<String, Value>>,
depth: u8,
) -> Option<Value> {
if depth == 0 {
return None;
}
match schema {
Value::Object(object) => {
if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
let name = reference
.strip_prefix("#/$defs/")
.or_else(|| reference.strip_prefix("#/definitions/"))?;
let definition = definitions?.get(name)?;
return dereferenced_payload_subschema(definition, definitions, depth - 1);
}
let mut out = serde_json::Map::new();
for (key, value) in object {
out.insert(
key.clone(),
dereferenced_payload_subschema(value, definitions, depth)?,
);
}
Some(Value::Object(out))
}
Value::Array(items) => Some(Value::Array(
items
.iter()
.map(|item| dereferenced_payload_subschema(item, definitions, depth))
.collect::<Option<_>>()?,
)),
other => Some(other.clone()),
}
}
pub(super) fn is_unconditional_self_presence_overlay(
target_value_path: &helm_schema_core::ValuesPath,
overlay: &ConditionalPathOverlay,
) -> bool {
matches!(
overlay.guards.as_slice(),
[guard]
if matches!(guard, ConditionalGuard::Not(_))
&& guard.is_self_presence_for(target_value_path)
)
}
pub(super) fn is_bare_iterable_implication(
implication: &helm_schema_core::ContractRequirementImplication,
) -> bool {
matches!(
&implication.target,
helm_schema_core::ContractRequirementTarget::Value
) && matches!(
implication.requirements.as_slice(),
[helm_schema_core::FailValueRequirement::Iterable { .. }]
)
}
pub(super) fn member_implication_covers_range_domain(
implications: &[helm_schema_core::ContractRequirementImplication],
guards: &[ConditionalGuard],
) -> bool {
implications.iter().any(|implication| {
implication.outer_guards == guards
&& matches!(
&implication.target,
helm_schema_core::ContractRequirementTarget::Members { .. }
| helm_schema_core::ContractRequirementTarget::MembersExceptKeys { .. }
| helm_schema_core::ContractRequirementTarget::MembersWhereEquals { .. }
)
})
}
pub(super) fn implication_has_self_truthy_guard(
implication: &helm_schema_core::ContractRequirementImplication,
target_value_path: &helm_schema_core::ValuesPath,
) -> bool {
implication
.outer_guards
.iter()
.any(|guard| guard.is_self_truthy_for(target_value_path))
}
pub(super) fn implication_has_self_presence_guard(
implication: &helm_schema_core::ContractRequirementImplication,
target_value_path: &helm_schema_core::ValuesPath,
) -> bool {
implication
.outer_guards
.iter()
.any(|guard| guard.is_self_presence_for(target_value_path))
}
pub(super) fn resolved_schema_admits_fail_requirement_domain(
resolved_schema: &Value,
implication: &helm_schema_core::ContractRequirementImplication,
) -> bool {
!crate::schema_model::is_empty_schema(resolved_schema)
&& fail_requirement_runtime_types(implication)
.is_subset(&schema_runtime_types(resolved_schema))
}
fn fail_requirement_runtime_types(
implication: &helm_schema_core::ContractRequirementImplication,
) -> BTreeSet<&'static str> {
use helm_schema_core::ContractRequirementTarget;
match &implication.target {
ContractRequirementTarget::Members { allow_integer }
| ContractRequirementTarget::MembersExceptKeys { allow_integer, .. }
| ContractRequirementTarget::MembersAt { allow_integer, .. }
| ContractRequirementTarget::MembersAtWhereTruthy { allow_integer, .. } => {
let mut types = BTreeSet::from(["array", "null", "object"]);
if *allow_integer {
types.insert("integer");
}
types
}
ContractRequirementTarget::MembersMatchingPrefix { .. }
| ContractRequirementTarget::MembersWhereEquals { .. } => {
BTreeSet::from(["array", "null", "object"])
}
ContractRequirementTarget::Keys => BTreeSet::from(["array", "null", "object"]),
ContractRequirementTarget::Value => crate::requirement_domain::admitted_json_value_kinds(
&implication.requirements,
crate::requirement_domain::RequirementPosition::WholeValue,
)
.into_iter()
.map(json_kind_runtime_type)
.collect(),
}
}
fn json_kind_runtime_type(kind: crate::requirement_domain::JsonValueKind) -> &'static str {
match kind {
crate::requirement_domain::JsonValueKind::Null => "null",
crate::requirement_domain::JsonValueKind::Boolean => "boolean",
crate::requirement_domain::JsonValueKind::Integer => "integer",
crate::requirement_domain::JsonValueKind::NonIntegerNumber => "number",
crate::requirement_domain::JsonValueKind::String => "string",
crate::requirement_domain::JsonValueKind::Array => "array",
crate::requirement_domain::JsonValueKind::Object => "object",
}
}
pub(crate) fn schema_runtime_types(schema: &Value) -> BTreeSet<&'static str> {
let all_types = || {
BTreeSet::from([
"array", "boolean", "integer", "null", "number", "object", "string",
])
};
let Some(object) = schema.as_object() else {
return if schema.as_bool() == Some(false) {
BTreeSet::new()
} else {
all_types()
};
};
let mut types = match object.get("type") {
Some(Value::String(schema_type)) => runtime_types_for_declared_type(schema_type),
Some(Value::Array(schema_types)) => schema_types
.iter()
.filter_map(Value::as_str)
.flat_map(runtime_types_for_declared_type)
.collect(),
_ => all_types(),
};
if let Some(value) = object.get("const") {
let const_types = BTreeSet::from([runtime_type_for_value(value)]);
types = types.intersection(&const_types).copied().collect();
}
if let Some(values) = object.get("enum").and_then(Value::as_array) {
let enum_types = values.iter().map(runtime_type_for_value).collect();
types = types.intersection(&enum_types).copied().collect();
}
for keyword in ["anyOf", "oneOf"] {
if let Some(arms) = object.get(keyword).and_then(Value::as_array) {
let arm_types = arms.iter().flat_map(schema_runtime_types).collect();
types = types.intersection(&arm_types).copied().collect();
}
}
if let Some(arms) = object.get("allOf").and_then(Value::as_array) {
for arm in arms {
let arm_types = schema_runtime_types(arm);
types = types.intersection(&arm_types).copied().collect();
}
}
types
}
fn runtime_type_for_value(value: &Value) -> &'static str {
match value {
Value::Array(_) => "array",
Value::Bool(_) => "boolean",
Value::Null => "null",
Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
Value::Number(_) => "number",
Value::Object(_) => "object",
Value::String(_) => "string",
}
}
fn runtime_types_for_declared_type(schema_type: &str) -> BTreeSet<&'static str> {
match schema_type {
"array" => BTreeSet::from(["array"]),
"boolean" => BTreeSet::from(["boolean"]),
"integer" => BTreeSet::from(["integer"]),
"null" => BTreeSet::from(["null"]),
"number" => BTreeSet::from(["integer", "number"]),
"object" => BTreeSet::from(["object"]),
"string" => BTreeSet::from(["string"]),
_ => BTreeSet::new(),
}
}
pub(super) fn relax_required_members_supplied_by_default(schema: &mut Value, default: &YamlValue) {
let (Some(schema), YamlValue::Mapping(defaults)) = (schema.as_object_mut(), default) else {
return;
};
if let Some(required) = schema.get_mut("required").and_then(Value::as_array_mut) {
required.retain(|member| {
member
.as_str()
.is_none_or(|member| !defaults.contains_key(YamlValue::String(member.to_string())))
});
if required.is_empty() {
schema.remove("required");
}
}
if let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) {
for (member, member_schema) in properties {
if let Some(member_default) = defaults.get(YamlValue::String(member.clone())) {
relax_required_members_supplied_by_default(member_schema, member_default);
}
}
}
for keyword in ["allOf", "anyOf", "oneOf"] {
let Some(branches) = schema.get_mut(keyword).and_then(Value::as_array_mut) else {
continue;
};
for branch in branches {
relax_required_members_supplied_by_default(branch, default);
}
}
}