use serde_json::Value;
use helm_schema_core::{
ConditionalGuard, ConditionalPathOverlay, ContractValuePathFacts, GuardValue, ValueKind,
ValuesPath,
};
use serde_yaml::Value as YamlValue;
use crate::condition_encoding::{
HELM_TRUTHY_DEFINITION_NAME, evaluate_guard_set_on_values, helm_truthy_definition_schema,
value_references_helm_truthy,
};
use crate::foreign_schema::ForeignSchemaRestriction;
use crate::merge::{merge_schema_list, merge_two_schemas, union_schema_list};
use crate::path_schema::{
generalize_fixed_object_schema_to_open_map, merge_explicit_empty_placeholder,
open_fragment_values_schema,
};
use crate::schema_model::{
add_null_schema, empty_schema, empty_string_schema, guard_value_to_json, is_annotation_keyword,
is_declared_object_schema, is_empty_schema, is_object_or_array_schema,
is_open_string_map_schema, is_scalar_like_schema, is_scalar_schema, scalar_union_schema,
schema_allows_type, schema_permits_empty_string, schema_type, type_schema,
};
use crate::schema_node::SchemaNode;
use crate::schema_node::is_placeholder_fragment_object_schema;
use crate::values_yaml::ValuesYamlPathFacts;
use crate::values_yaml::yaml_value_at_values_path;
pub(crate) const PLAIN_SCALAR_NULL_TOKEN_PATTERN: &str = r"^(|~|null|Null|NULL)$";
pub(crate) const PLAIN_SCALAR_BOOL_TOKEN_PATTERN: &str =
r"^(true|True|TRUE|false|False|FALSE|yes|Yes|YES|no|No|NO|on|On|ON|off|Off|OFF|y|Y|n|N)$";
const PLAIN_SCALAR_DECIMAL_NUMBER_TOKEN_PATTERN: &str = r"^([0-9][0-9_]{0,50}(\.[0-9_]{0,50})?([eE][+-]?[0-9]{1,2})?|[+-]_*[0-9][0-9_]{0,50}(\.[0-9_]{0,50})?([eE][+-]?[0-9]{1,2})?|[+-]_*\._*[0-9][0-9_]{0,50}([eE][+-]?[0-9]{1,2})?|\.[0-9]{1,50}([eE][+-]?[0-9]{1,2})?)$";
pub(crate) const PLAIN_SCALAR_SPECIAL_FLOAT_TOKEN_PATTERN: &str =
r"^([+-]?\.(inf|Inf|INF)|\.(nan|NaN|NAN))$";
const PLAIN_SCALAR_INTEGER_SLOT_TOKEN_PATTERN: &str = r"^(([+-]_*)?(0|[1-9][0-9_]{0,17}|0[xX][0-9a-fA-F]{1,15}|0[bB][01]{1,62}|0[oO][0-7]{1,20}|0[0-7]{1,20})|[+-]_*0[0-7]{0,8}[89][0-9]{0,8})$";
pub(crate) const PLAIN_SCALAR_NUMBER_TOKEN_PATTERN: &str = r"^(([+-]_*)?(0|[1-9][0-9_]{0,17}|0[xX][0-9a-fA-F]{1,15}|0[bB][01]{1,62}|0[oO][0-7]{1,20}|0[0-7]{1,20})|[0-9][0-9_]{0,50}(\.[0-9_]{0,50})?([eE][+-]?[0-9]{1,2})?|[+-]_*[0-9][0-9_]{0,50}(\.[0-9_]{0,50})?([eE][+-]?[0-9]{1,2})?|[+-]_*\._*[0-9][0-9_]{0,50}([eE][+-]?[0-9]{1,2})?|\.[0-9]{1,50}([eE][+-]?[0-9]{1,2})?)$";
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct ResolvePolicy;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ValuePathSchemaFacts {
pub(crate) contract: ContractValuePathFacts,
pub(crate) values_yaml: ValuesYamlPathFacts,
}
impl ValuePathSchemaFacts {
pub(crate) fn new(contract: ContractValuePathFacts, values_yaml: ValuesYamlPathFacts) -> Self {
Self {
contract,
values_yaml,
}
}
fn has_explicit_null_scalar_default(
self,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> bool {
self.values_yaml.is_explicit_null
&& (is_scalar_like_schema(guard_predicate_schema)
|| (!self.contract.has_render_use && is_scalar_like_schema(type_hint_schema)))
}
fn accepts_null_default(
self,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> bool {
self.contract.is_nullable
|| self.has_explicit_null_scalar_default(type_hint_schema, guard_predicate_schema)
}
fn preserve_explicit_null_default(
self,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> bool {
self.values_yaml.is_explicit_null
&& self.accepts_null_default(type_hint_schema, guard_predicate_schema)
}
fn preserve_empty_string_fallback(
self,
provider_schema: &Value,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> bool {
self.values_yaml.is_empty_string
&& ((self.contract.has_render_use
&& self.contract.all_render_uses_self_guarded.holds())
|| schema_allows_type(provider_schema, "string")
|| is_scalar_like_schema(type_hint_schema)
|| is_scalar_like_schema(guard_predicate_schema))
}
fn empty_map_placeholder_has_structural_object_use(self, provider_schema: &Value) -> bool {
self.values_yaml.is_empty_map
&& !self.contract.used_as_serialized
&& !self.contract.has_parsed_map_layered_use
&& (self.contract.is_ranged_source
|| self.contract.has_self_range_guard_render_use
|| self.contract.used_as_yaml_serialized
|| (schema_allows_type(provider_schema, "object")
&& (self.contract.used_as_fragment
|| (self.contract.has_render_use
&& self.contract.all_render_uses_self_guarded.holds()))))
}
}
pub(crate) struct ValuePathSchemaInputs {
pub(crate) facts: ValuePathSchemaFacts,
pub(crate) provider_schema: Value,
pub(crate) values_yaml_schema: Value,
pub(crate) guard_predicate_schema: Value,
pub(crate) type_hint_schema: Value,
pub(crate) guarded_type_hint_schema: Value,
pub(crate) fallback_type_hint_schema: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ProviderValueUsePolicy {
kind: ValueKind,
stringified: bool,
is_self_range_collection: bool,
template_supplied_member_keys: std::collections::BTreeSet<String>,
split_segment: Option<helm_schema_core::SplitSegmentUse>,
omitted_members: std::collections::BTreeMap<String, Vec<ConditionalGuard>>,
}
impl ProviderValueUsePolicy {
pub(crate) fn new(
kind: ValueKind,
stringified: bool,
is_self_range_collection: bool,
template_supplied_member_keys: std::collections::BTreeSet<String>,
split_segment: Option<helm_schema_core::SplitSegmentUse>,
omitted_members: std::collections::BTreeMap<String, Vec<ConditionalGuard>>,
) -> Self {
Self {
kind,
stringified,
is_self_range_collection,
template_supplied_member_keys,
split_segment,
omitted_members,
}
}
}
impl ResolvePolicy {
pub(crate) fn provider_schema_for_value_use(
schema: &Value,
policy: &ProviderValueUsePolicy,
) -> Option<Value> {
match policy.kind {
ValueKind::TemplatedYamlSerialized => {
Some(templated_yaml_provider_preimage(subtract_omitted_members(
relax_template_supplied_required(
schema.clone(),
&policy.template_supplied_member_keys,
),
&policy.omitted_members,
)))
}
ValueKind::YamlSerialized => Some(subtract_omitted_members(
relax_template_supplied_required(
schema.clone(),
&policy.template_supplied_member_keys,
),
&policy.omitted_members,
)),
ValueKind::Fragment if policy.is_self_range_collection => {
schema_allows_type(schema, "array")
.then(|| crate::schema_model::type_union_schema(["array", "object"]))
}
ValueKind::Fragment => schema_allows_type(schema, "array").then(|| schema.clone()),
ValueKind::PartialScalar | ValueKind::Serialized | ValueKind::WidenedDependency => None,
ValueKind::Scalar if policy.is_self_range_collection => {
ForeignSchemaRestriction::ScalarCollection.apply(schema.clone())
}
ValueKind::Scalar if policy.split_segment.is_some() => policy
.split_segment
.as_ref()
.and_then(|segment| split_segment_provider_preimage(schema, segment)),
ValueKind::Scalar if policy.stringified => {
Some(stringified_plain_scalar_provider_preimage(schema.clone()))
}
ValueKind::Scalar => ForeignSchemaRestriction::Scalar
.apply(schema.clone())
.map(plain_scalar_provider_preimage),
}
}
pub(crate) fn guard_predicate_schema(
value_path: &str,
predicate: &ConditionalGuard,
) -> Option<Value> {
let value_path = ValuesPath::parse(value_path);
match predicate {
ConditionalGuard::Eq { path, value } if path == &value_path => {
if matches!(value, GuardValue::Null) {
return Some(empty_schema());
}
let value = guard_value_to_json(value)?;
let value_type = schema_type_for_guard_value(&value)?;
Some(
SchemaNode::any_of(vec![
SchemaNode::enum_values(vec![value]),
SchemaNode::type_named(value_type),
SchemaNode::type_named("null"),
])
.into_value(),
)
}
ConditionalGuard::TypeIs { path, schema_type } if path == &value_path => {
match schema_type.as_str() {
"array" | "boolean" | "integer" | "number" | "object" | "string" => {
Some(type_schema(schema_type))
}
_ => None,
}
}
ConditionalGuard::Truthy { .. }
| ConditionalGuard::With { .. }
| ConditionalGuard::Eq { .. }
| ConditionalGuard::NotEq { .. }
| ConditionalGuard::Absent { .. }
| ConditionalGuard::ContainsMemberEquals { .. }
| ConditionalGuard::ContainsTruthyMember { .. }
| ConditionalGuard::ContainsEquals { .. }
| ConditionalGuard::TypeIs { .. }
| ConditionalGuard::MatchesPattern { .. }
| ConditionalGuard::IntGt { .. }
| ConditionalGuard::IntLt { .. }
| ConditionalGuard::HasKey { .. }
| ConditionalGuard::AtMostOneMember { .. }
| ConditionalGuard::MinMembers { .. }
| ConditionalGuard::Not(_)
| ConditionalGuard::AllOf(_)
| ConditionalGuard::AnyOf(_) => None,
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
)]
pub(crate) fn resolve_schema_for_value_path(input: ValuePathSchemaInputs) -> Value {
let ValuePathSchemaInputs {
facts,
provider_schema,
values_yaml_schema,
guard_predicate_schema,
type_hint_schema,
guarded_type_hint_schema,
fallback_type_hint_schema,
} = input;
let fallback_type_hint_schema = if facts.contract.used_as_serialized {
empty_schema()
} else {
fallback_type_hint_schema
};
let fallback_hint_only_typing = !is_empty_schema(&fallback_type_hint_schema)
&& is_empty_schema(&type_hint_schema)
&& is_empty_schema(&provider_schema)
&& is_empty_schema(&guard_predicate_schema)
&& !facts.contract.has_string_contract;
let type_hint_schema = merge_two_schemas(type_hint_schema, fallback_type_hint_schema);
let control_only_without_contract = !facts.contract.has_non_control_use
&& !facts.contract.has_referenced_descendants
&& !facts.contract.has_item_descendants
&& !facts.contract.has_structured_item_descendants
&& is_empty_schema(&provider_schema)
&& is_empty_schema(&type_hint_schema)
&& is_empty_schema(&guarded_type_hint_schema);
let values_yaml_schema =
if control_only_without_contract || facts.contract.used_as_serialized {
empty_schema()
} else if facts.contract.is_partial_scalar_value_path
&& is_scalar_schema(&values_yaml_schema)
{
scalar_union_schema()
} else {
values_yaml_schema
};
let mut guard_predicate_schema = if facts.contract.has_unconditional_render_use {
empty_schema()
} else {
guard_predicate_schema
};
let deferred_guard_schema = if facts.contract.used_as_serialized {
std::mem::replace(&mut guard_predicate_schema, empty_schema())
} else {
empty_schema()
};
let preserve_explicit_null_default_by_contract =
facts.preserve_explicit_null_default(&type_hint_schema, &guard_predicate_schema);
let preserve_empty_string_fallback = facts.preserve_empty_string_fallback(
&provider_schema,
&type_hint_schema,
&guard_predicate_schema,
);
let values_yaml_schema = Self::adjust_values_yaml_schema_for_value_path(
values_yaml_schema,
facts,
&provider_schema,
);
let provider_schema = Self::adjust_provider_schema_for_value_path(
facts,
provider_schema,
&values_yaml_schema,
&type_hint_schema,
&guard_predicate_schema,
);
let preserve_common_plain_string =
schema_covers_strict_plain_scalar_string(&provider_schema);
let partial_scalar_schema = Self::partial_scalar_schema_for_value_path(
facts,
&provider_schema,
&type_hint_schema,
&guard_predicate_schema,
);
let guard_predicate_schema =
merge_schema_list(vec![guard_predicate_schema, partial_scalar_schema]);
let merged = Self::resolve_merged_schema_for_value_path(
ValuePathSchemaInputs {
facts,
provider_schema,
values_yaml_schema,
guard_predicate_schema,
type_hint_schema,
guarded_type_hint_schema: empty_schema(),
fallback_type_hint_schema: empty_schema(),
},
preserve_empty_string_fallback,
);
let merged =
if preserve_common_plain_string && !schema_covers_strict_plain_scalar_string(&merged) {
union_schema_list(vec![merged, strict_plain_scalar_string_schema()])
} else {
merged
};
let widening_schema = merge_two_schemas(guarded_type_hint_schema, deferred_guard_schema);
let merged = if !is_empty_schema(&merged) && !is_empty_schema(&widening_schema) {
union_schema_list(vec![merged, widening_schema])
} else {
merged
};
let merged = if !is_empty_schema(&merged)
&& !facts.contract.is_direct_ranged_source
&& !facts.contract.has_non_self_guarded_string_contract
&& ((facts.contract.has_render_use
&& ((facts.contract.all_render_uses_self_guarded.holds()
&& !facts.contract.has_unconditional_render_use)
|| (facts.contract.all_render_uses_falsy_tolerant.holds()
&& !facts.contract.has_referenced_descendants)))
|| fallback_hint_only_typing)
{
union_schema_list(vec![merged, helm_falsy_schema()])
} else {
merged
};
let preserve_explicit_null_default = preserve_explicit_null_default_by_contract
|| (facts.values_yaml.is_explicit_null
&& facts.contract.used_as_fragment
&& !is_empty_schema(&merged));
let self_guarded_structure_tolerates_null = facts.contract.is_nullable
&& facts.contract.has_render_use
&& facts.contract.all_render_uses_self_guarded.holds()
&& is_object_or_array_schema(&merged);
let dependency_default_tolerates_null = facts.values_yaml.has_dependency_default
&& !facts.contract.accepted_dependency_values_root_fragment
&& !facts.contract.has_unconditional_render_use;
let nullable_scalar_without_strict_raw_consumer = is_scalar_like_schema(&merged)
&& facts.contract.is_nullable
&& !facts.contract.has_non_self_guarded_string_contract;
let resolved = if (preserve_explicit_null_default
|| nullable_scalar_without_strict_raw_consumer
|| self_guarded_structure_tolerates_null
|| dependency_default_tolerates_null)
&& !is_empty_schema(&merged)
{
add_null_schema(merged)
} else if preserve_explicit_null_default {
empty_schema()
} else if facts.empty_map_placeholder_has_structural_object_use(&merged) {
let merged = if facts.contract.has_merge_layered_use {
crate::merge::union_schema_list(vec![
merged,
serde_json::json!({ "additionalProperties": {}, "type": "object" }),
])
} else {
merged
};
merge_explicit_empty_placeholder(
merged,
facts.values_yaml.is_empty_map,
facts.contract.has_structured_item_descendants
&& !facts.contract.has_destructured_range_use,
facts.contract.has_render_use
&& facts.contract.all_render_uses_self_guarded.holds()
&& !facts.contract.has_merge_layered_use,
facts.contract.used_as_fragment && !facts.contract.is_ranged_source,
)
} else if facts.values_yaml.has_no_schema_evidence && facts.contract.is_ranged_source {
crate::path_schema::stamp_explicit_map_openness(merged)
} else if facts.contract.used_as_serialized
&& facts.contract.has_referenced_descendants
&& is_empty_schema(&merged)
{
serde_json::json!({ "additionalProperties": {} })
} else {
merged
};
if facts.contract.is_direct_ranged_source {
let iterable = crate::runtime_iterable_schema(
!facts.contract.has_destructured_range_use
&& !facts.contract.has_json_decoded_range_use,
);
if is_empty_schema(&resolved) {
iterable
} else {
union_schema_list(vec![resolved, iterable])
}
} else {
resolved
}
}
fn adjust_values_yaml_schema_for_value_path(
values_yaml_schema: Value,
facts: ValuePathSchemaFacts,
provider_schema: &Value,
) -> Value {
let values_yaml_schema =
if facts.empty_map_placeholder_has_structural_object_use(provider_schema) {
empty_schema()
} else {
values_yaml_schema
};
let values_yaml_schema =
if facts.contract.accepted_values_root_fragment && facts.values_yaml.is_mapping {
values_yaml_schema
} else if facts.contract.used_as_fragment
&& is_empty_schema(provider_schema)
&& should_open_fragment_values_schema(&values_yaml_schema, facts)
{
open_fragment_values_schema(values_yaml_schema)
} else {
values_yaml_schema
};
if facts.contract.is_ranged_source && facts.values_yaml.is_mapping {
generalize_fixed_object_schema_to_open_map(values_yaml_schema)
} else {
values_yaml_schema
}
}
fn adjust_provider_schema_for_value_path(
facts: ValuePathSchemaFacts,
provider_schema: Value,
values_yaml_schema: &Value,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> Value {
if facts.contract.used_as_fragment
&& is_scalar_schema(values_yaml_schema)
&& (is_scalar_like_schema(type_hint_schema)
|| is_scalar_like_schema(guard_predicate_schema))
{
ForeignSchemaRestriction::Scalar
.apply(provider_schema.clone())
.unwrap_or(provider_schema)
} else {
provider_schema
}
}
fn partial_scalar_schema_for_value_path(
facts: ValuePathSchemaFacts,
provider_schema: &Value,
type_hint_schema: &Value,
guard_predicate_schema: &Value,
) -> Value {
if facts.contract.is_partial_scalar_value_path
&& !facts.contract.used_as_serialized
&& is_empty_schema(provider_schema)
&& is_empty_schema(type_hint_schema)
&& is_empty_schema(guard_predicate_schema)
&& facts.values_yaml.has_no_schema_evidence
{
scalar_union_schema()
} else {
empty_schema()
}
}
fn resolve_merged_schema_for_value_path(
input: ValuePathSchemaInputs,
preserve_empty_string_fallback: bool,
) -> Value {
let base = if !is_empty_schema(&input.provider_schema) {
if is_empty_schema(&input.values_yaml_schema) {
input.provider_schema
} else {
if input.facts.contract.has_referenced_descendants
&& is_declared_object_schema(&input.values_yaml_schema)
&& is_scalar_schema(&input.provider_schema)
{
input.values_yaml_schema
} else if input.facts.contract.used_as_fragment
&& is_declared_object_schema(&input.values_yaml_schema)
&& is_open_string_map_schema(&input.provider_schema)
{
input.provider_schema
} else if input.facts.contract.used_as_fragment
&& is_scalar_schema(&input.values_yaml_schema)
&& is_object_or_array_schema(&input.provider_schema)
{
input.values_yaml_schema
} else if let Some(values_yaml_ty) = schema_type(&input.values_yaml_schema)
&& is_scalar_schema(&input.values_yaml_schema)
&& schema_allows_type(&input.provider_schema, values_yaml_ty)
{
if preserve_empty_string_fallback
&& values_yaml_ty == "string"
&& !schema_permits_empty_string(&input.provider_schema)
{
union_schema_list(vec![input.provider_schema, empty_string_schema()])
} else {
input.provider_schema
}
} else {
merge_two_schemas(input.provider_schema, input.values_yaml_schema)
}
}
} else if input.facts.contract.used_as_fragment
&& !input.facts.contract.used_as_serialized
&& (is_empty_schema(&input.values_yaml_schema) || input.facts.values_yaml.is_empty_map)
{
empty_schema()
} else if !is_empty_schema(&input.values_yaml_schema) {
input.values_yaml_schema
} else {
empty_schema()
};
let base = merge_two_schemas(base, input.type_hint_schema);
if is_empty_schema(&base) || is_empty_schema(&input.guard_predicate_schema) {
merge_two_schemas(base, input.guard_predicate_schema)
} else {
union_schema_list(vec![base, input.guard_predicate_schema])
}
}
}
fn relax_template_supplied_required(
mut schema: Value,
supplied: &std::collections::BTreeSet<String>,
) -> Value {
if supplied.is_empty() {
return schema;
}
if let Some(object) = schema.as_object_mut() {
if let Some(required) = object.get_mut("required").and_then(Value::as_array_mut) {
required.retain(|key| key.as_str().is_none_or(|key| !supplied.contains(key)));
if required.is_empty() {
object.remove("required");
}
}
for arms_key in ["allOf", "anyOf", "oneOf"] {
if let Some(arms) = object.get_mut(arms_key).and_then(Value::as_array_mut) {
for arm in arms {
*arm = relax_template_supplied_required(arm.take(), supplied);
}
}
}
}
schema
}
fn subtract_omitted_members(
mut schema: Value,
omitted: &std::collections::BTreeMap<String, Vec<helm_schema_core::ConditionalGuard>>,
) -> Value {
if omitted.is_empty() {
return schema;
}
if let Some(object) = schema.as_object_mut() {
if object.contains_key("properties")
|| object.get("additionalProperties") == Some(&Value::Bool(false))
{
let properties = object
.entry("properties")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Some(properties) = properties.as_object_mut() {
for key in omitted.keys() {
properties.insert(key.clone(), empty_schema());
}
}
}
if let Some(required) = object.get_mut("required").and_then(Value::as_array_mut) {
required.retain(|key| key.as_str().is_none_or(|key| !omitted.contains_key(key)));
if required.is_empty() {
object.remove("required");
}
}
for arms_key in ["allOf", "anyOf", "oneOf"] {
if let Some(arms) = object.get_mut(arms_key).and_then(Value::as_array_mut) {
for arm in arms {
*arm = subtract_omitted_members(arm.take(), omitted);
}
}
}
}
schema
}
mod scalar_preimage;
#[cfg(test)]
pub(crate) use scalar_preimage::plain_scalar_safe_comment_string_schema;
use scalar_preimage::{
helm_falsy_schema, plain_scalar_provider_preimage, split_segment_provider_preimage,
stringified_plain_scalar_provider_preimage, templated_yaml_provider_preimage,
};
pub(crate) use scalar_preimage::{
plain_scalar_structural_exclusions, printf_string_formattable_mapping_schema,
printf_string_formattable_string_schema, schema_covers_strict_plain_scalar_string,
split_segment_pattern, strict_plain_scalar_string_schema,
};
pub(crate) fn conditional_target_schema(
target_value_path: &ValuesPath,
overlay: &ConditionalPathOverlay,
values_yaml_doc: &YamlValue,
branch_schema: Value,
values_yaml_schema: &Value,
resolved_fallback: Value,
active_by_defaults: Option<bool>,
) -> Value {
let schema = conditional_target_schema_inner(
target_value_path,
overlay,
values_yaml_doc,
branch_schema,
values_yaml_schema,
resolved_fallback,
active_by_defaults,
);
let facts = overlay.evidence.facts;
if facts.has_render_use
&& facts.all_render_uses_self_guarded.holds()
&& !facts.has_unconditional_render_use
&& !facts.is_direct_ranged_source
&& !crate::schema_model::is_empty_schema(&schema)
{
return union_schema_list(vec![schema, helm_falsy_schema()]);
}
schema
}
fn conditional_target_schema_inner(
target_value_path: &ValuesPath,
overlay: &ConditionalPathOverlay,
values_yaml_doc: &YamlValue,
branch_schema: Value,
values_yaml_schema: &Value,
resolved_fallback: Value,
active_by_defaults: Option<bool>,
) -> Value {
let declared_default = yaml_value_at_values_path(values_yaml_doc, target_value_path)
.and_then(|value| serde_json::to_value(value).ok());
let self_guard_excludes_declared_default =
self_guards_exclude_declared_default(target_value_path, overlay, values_yaml_doc);
let rejects_declared_default = |schema: &Value| {
declared_default
.as_ref()
.is_some_and(|default_value| !schema_accepts_json_value(schema, default_value))
};
let branch_schema =
preserve_positive_self_type_domains(target_value_path, overlay, branch_schema);
let self_type_complement = overlay.guards.iter().any(|guard| {
matches!(
guard,
ConditionalGuard::Not(inner)
if matches!(
inner.as_ref(),
ConditionalGuard::TypeIs { path, .. }
if path == target_value_path
)
)
});
let branch_schema = if !self_guard_excludes_declared_default
&& active_by_defaults.is_some()
&& (!(overlay.evidence.facts.used_as_serialized
|| overlay.evidence.facts.used_as_yaml_serialized)
|| (self_type_complement && active_by_defaults == Some(true)))
&& !(overlay.evidence.facts.used_as_fragment
&& is_placeholder_fragment_object_schema(values_yaml_schema))
&& should_merge_values_yaml_into_conditional_branch(&branch_schema, values_yaml_schema)
{
merge_schema_list(vec![branch_schema, values_yaml_schema.clone()])
} else {
branch_schema
};
let branch_schema = if !self_guard_excludes_declared_default
&& rejects_declared_default(&branch_schema)
{
declared_default.as_ref().map_or_else(
|| branch_schema.clone(),
|default_value| {
if default_value.is_null() && overlay.evidence.facts.is_nullable {
return union_schema_list(vec![branch_schema.clone(), type_schema("null")]);
}
let declared_type = if default_value.is_object() {
Some("object")
} else if default_value.is_array() {
Some("array")
} else {
None
};
if declared_type
.is_some_and(|schema_type| !schema_allows_type(&branch_schema, schema_type))
{
union_schema_list(vec![
branch_schema.clone(),
open_objects_rejecting_declared_members(
values_yaml_schema.clone(),
default_value,
),
])
} else {
open_objects_rejecting_declared_members(branch_schema.clone(), default_value)
}
},
)
} else {
branch_schema
};
if active_by_defaults != Some(true) {
if self_guard_excludes_declared_default {
return branch_schema;
}
if is_placeholder_fragment_object_schema(&branch_schema)
&& !is_placeholder_fragment_object_schema(&resolved_fallback)
{
return if rejects_declared_default(&resolved_fallback) {
branch_schema
} else {
resolved_fallback
};
}
if !overlay.evidence.facts.is_nullable {
return branch_schema;
}
}
if rejects_declared_default(&branch_schema) {
declared_default
.as_ref()
.map_or(resolved_fallback.clone(), |default_value| {
open_objects_rejecting_declared_members(resolved_fallback, default_value)
})
} else {
branch_schema
}
}
fn self_guards_exclude_declared_default(
target_value_path: &ValuesPath,
overlay: &ConditionalPathOverlay,
values_yaml_doc: &YamlValue,
) -> bool {
let self_guards = overlay
.guards
.iter()
.filter(|guard| {
let paths = guard.value_paths();
!paths.is_empty() && paths.iter().all(|path| path == target_value_path)
})
.cloned()
.collect::<Vec<_>>();
!self_guards.is_empty()
&& evaluate_guard_set_on_values(&self_guards, values_yaml_doc) == Some(false)
}
fn preserve_positive_self_type_domains(
target_value_path: &ValuesPath,
overlay: &ConditionalPathOverlay,
mut branch_schema: Value,
) -> Value {
let mut positive_self_types = std::collections::BTreeSet::new();
for guard in &overlay.guards {
collect_positive_self_types(guard, target_value_path, false, &mut positive_self_types);
}
for schema_type in positive_self_types {
if schema_type == "number" && schema_allows_non_falsy_type(&branch_schema, "integer") {
continue;
}
if !schema_allows_non_falsy_type(&branch_schema, &schema_type) {
branch_schema = union_schema_list(vec![branch_schema, type_schema(&schema_type)]);
}
}
branch_schema
}
fn collect_positive_self_types(
guard: &helm_schema_core::ConditionalGuard,
target_value_path: &ValuesPath,
negated: bool,
out: &mut std::collections::BTreeSet<String>,
) {
match guard {
helm_schema_core::ConditionalGuard::TypeIs { path, schema_type }
if !negated && path == target_value_path =>
{
out.insert(schema_type.clone());
}
helm_schema_core::ConditionalGuard::Not(inner) => {
collect_positive_self_types(inner, target_value_path, !negated, out);
}
helm_schema_core::ConditionalGuard::AllOf(guards)
| helm_schema_core::ConditionalGuard::AnyOf(guards) => {
for guard in guards {
collect_positive_self_types(guard, target_value_path, negated, out);
}
}
helm_schema_core::ConditionalGuard::Truthy { .. }
| helm_schema_core::ConditionalGuard::With { .. }
| helm_schema_core::ConditionalGuard::IntGt { .. }
| helm_schema_core::ConditionalGuard::IntLt { .. }
| helm_schema_core::ConditionalGuard::HasKey { .. }
| helm_schema_core::ConditionalGuard::ContainsMemberEquals { .. }
| helm_schema_core::ConditionalGuard::ContainsTruthyMember { .. }
| helm_schema_core::ConditionalGuard::ContainsEquals { .. }
| helm_schema_core::ConditionalGuard::Eq { .. }
| helm_schema_core::ConditionalGuard::NotEq { .. }
| helm_schema_core::ConditionalGuard::Absent { .. }
| helm_schema_core::ConditionalGuard::TypeIs { .. }
| helm_schema_core::ConditionalGuard::MatchesPattern { .. }
| helm_schema_core::ConditionalGuard::AtMostOneMember { .. }
| helm_schema_core::ConditionalGuard::MinMembers { .. } => {}
}
}
fn schema_allows_non_falsy_type(schema: &Value, schema_type: &str) -> bool {
if SchemaNode::from_value(schema.clone())
.is_not_reference(&format!("#/$defs/{HELM_TRUTHY_DEFINITION_NAME}"))
{
return false;
}
for keyword in ["anyOf", "oneOf"] {
if let Some(arms) = schema.get(keyword).and_then(Value::as_array) {
return arms
.iter()
.any(|arm| schema_allows_non_falsy_type(arm, schema_type));
}
}
schema_allows_type(schema, schema_type)
}
mod declared_default;
pub(crate) use declared_default::{
open_objects_rejecting_declared_members, preserve_declared_default_in_schema,
};
use declared_default::{
schema_accepts_json_value, schema_type_for_guard_value,
should_merge_values_yaml_into_conditional_branch, should_open_fragment_values_schema,
};