mod base_schema;
mod condition_encoding;
mod foreign_schema;
mod merge;
mod overlay_lowering;
mod path_resolver;
mod path_schema;
mod program_wrapper;
mod provider_definitions;
mod provider_schema;
mod quoted_serialization;
pub mod required_inference;
mod required_source_backprojection;
mod resolve_policy;
mod schema_model;
mod schema_node;
mod schema_tree;
mod values_yaml;
use std::collections::{BTreeMap, BTreeSet};
use helm_schema_core::{ContractSchemaSignals, ResourceSchemaOracle};
use serde_json::Value;
use serde_yaml::Value as YamlValue;
use base_schema::{ConditionalTargetIndex, classify_base};
use condition_encoding::{
HELM_TRUTHY_DEFINITION_NAME, helm_truthy_definition_schema, value_references_helm_truthy,
};
use overlay_lowering::{
append_conditional_schemas, append_terminal_clauses, collect_conditional_schemas,
};
use path_resolver::PathSchemaResolver;
use provider_definitions::{
extract_provider_definitions, extract_repeated_provider_payloads, insert_definitions_into_root,
};
use schema_tree::{SchemaDocument, draft07_root_document};
#[derive(Clone, Copy)]
pub struct ValuesSchemaInput<'a> {
pub contract_schema_signals: &'a ContractSchemaSignals,
pub provider: &'a dyn ResourceSchemaOracle,
pub values_yaml: Option<&'a str>,
pub dependency_values_yaml: Option<&'a str>,
pub values_descriptions: Option<&'a BTreeMap<String, String>>,
}
impl<'a> ValuesSchemaInput<'a> {
pub fn new(
contract_schema_signals: &'a ContractSchemaSignals,
provider: &'a dyn ResourceSchemaOracle,
) -> Self {
Self {
contract_schema_signals,
provider,
values_yaml: None,
dependency_values_yaml: None,
values_descriptions: None,
}
}
#[must_use]
pub fn with_values_yaml(mut self, values_yaml: Option<&'a str>) -> Self {
self.values_yaml = values_yaml;
self
}
#[must_use]
pub fn with_dependency_values_yaml(mut self, dependency_values_yaml: Option<&'a str>) -> Self {
self.dependency_values_yaml = dependency_values_yaml;
self
}
#[must_use]
pub fn with_values_descriptions(
mut self,
values_descriptions: &'a BTreeMap<String, String>,
) -> Self {
self.values_descriptions = Some(values_descriptions);
self
}
}
#[tracing::instrument(skip_all)]
pub fn generate_values_schema(input: ValuesSchemaInput<'_>) -> Value {
let empty_values_descriptions = BTreeMap::new();
let values_descriptions = input
.values_descriptions
.unwrap_or(&empty_values_descriptions);
let mut values_yaml_doc = input
.values_yaml
.and_then(|s| serde_yaml::from_str::<YamlValue>(s).ok())
.unwrap_or(YamlValue::Null);
values_yaml::apply_values_default_sources(
&mut values_yaml_doc,
input.contract_schema_signals.values_default_sources(),
);
let mut subchart_defaults_doc = input
.dependency_values_yaml
.and_then(|s| serde_yaml::from_str::<YamlValue>(s).ok())
.unwrap_or(YamlValue::Null);
values_yaml::copy_values_default_sources(
&mut subchart_defaults_doc,
&values_yaml_doc,
input.contract_schema_signals.values_default_sources(),
);
let root_schema = build_root_schema(
input.contract_schema_signals,
&values_yaml_doc,
&subchart_defaults_doc,
values_descriptions,
input.provider,
);
draft07_root_document(root_schema)
}
pub(crate) fn runtime_iterable_schema(allow_integer: bool) -> serde_json::Value {
let mut arms = vec![
serde_json::json!({ "type": "array" }),
serde_json::json!({ "type": "object" }),
];
if allow_integer {
arms.push(serde_json::json!({ "type": "integer" }));
}
arms.push(serde_json::json!({ "type": "null" }));
serde_json::json!({ "anyOf": arms })
}
#[tracing::instrument(skip_all)]
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
)]
fn build_root_schema(
contract_schema_signals: &ContractSchemaSignals,
values_yaml_doc: &YamlValue,
subchart_defaults_doc: &YamlValue,
values_descriptions: &BTreeMap<String, String>,
provider: &dyn ResourceSchemaOracle,
) -> Value {
let mut root_schema = SchemaDocument::new_root_object();
let path_resolver = PathSchemaResolver::new(contract_schema_signals, values_yaml_doc, provider);
let mut resolved_paths = path_resolver.resolve_all();
let mut conditional_schemas = collect_conditional_schemas(
&resolved_paths,
contract_schema_signals,
values_yaml_doc,
provider,
);
let provider_definitions = extract_provider_definitions(
&mut resolved_paths,
&mut conditional_schemas,
values_descriptions,
);
let conditional_targets = ConditionalTargetIndex::from_conditionals(&conditional_schemas);
let accepted_values_root_paths = contract_schema_signals
.schema_evidence_by_value_path()
.values()
.filter(|evidence| evidence.facts.accepted_values_root_fragment)
.map(|evidence| split_value_path(&evidence.value_path))
.collect::<Vec<_>>();
let no_owning_ancestors = BTreeSet::new();
let base_span = tracing::info_span!("base_path_insertion").entered();
let owning_paths = resolved_paths
.iter()
.filter(|resolved_path| {
classify_base(resolved_path, &conditional_targets, &no_owning_ancestors)
.owns_descendants()
})
.map(|resolved_path| resolved_path.path_segments.clone())
.collect::<BTreeSet<_>>();
for resolved_path in &resolved_paths {
let owner = classify_base(resolved_path, &conditional_targets, &owning_paths);
let Some(schema) = owner.schema(resolved_path) else {
continue;
};
if owner.replaces() {
root_schema.replace_path_schema(&resolved_path.path_segments, schema);
} else {
root_schema.insert_path_schema(&resolved_path.path_segments, schema);
}
}
drop(base_span);
append_conditional_schemas(
&mut root_schema,
conditional_schemas,
values_yaml_doc,
subchart_defaults_doc,
);
append_terminal_clauses(
&mut root_schema,
contract_schema_signals.terminal_clauses(),
values_yaml_doc,
subchart_defaults_doc,
);
let mut default_fill_skip_paths = conditional_targets.target_paths.clone();
for resolved_path in &resolved_paths {
if resolved_path.used_as_serialized {
default_fill_skip_paths.insert(resolved_path.path_segments.clone());
}
}
for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
if evidence.facts.used_as_yaml_serialized {
default_fill_skip_paths.insert(split_value_path(value_path));
}
}
for value_path in contract_schema_signals.direct_ranged_value_paths() {
default_fill_skip_paths.insert(split_value_path(value_path));
}
let fill_span = tracing::info_span!("default_fill_and_finish").entered();
{
let _span = tracing::info_span!("merge_missing_defaults").entered();
root_schema.merge_missing_values_yaml_defaults_under_roots(
values_yaml_doc,
&accepted_values_root_paths,
&default_fill_skip_paths,
);
}
root_schema.open_helm_global_namespace();
let mut root_schema = root_schema.into_value();
if let Ok(declared_defaults) = serde_json::to_value(values_yaml_doc)
&& declared_defaults.is_object()
{
let _span = tracing::info_span!("preserve_declared_defaults").entered();
root_schema =
resolve_policy::preserve_declared_default_in_schema(root_schema, &declared_defaults);
}
let mut provider_definitions = provider_definitions;
{
let _span = tracing::info_span!("extract_repeated_provider_payloads").entered();
provider_definitions.extend(extract_repeated_provider_payloads(&mut root_schema));
}
let truthy_span = tracing::info_span!("helm_truthy_scan").entered();
if value_references_helm_truthy(&root_schema)
|| provider_definitions
.values()
.any(value_references_helm_truthy)
{
provider_definitions.insert(
HELM_TRUTHY_DEFINITION_NAME.to_string(),
helm_truthy_definition_schema(),
);
}
for style in [
helm_schema_core::QuotedScalarStyle::Double,
helm_schema_core::QuotedScalarStyle::Single,
] {
if quoted_serialization::value_references(&root_schema, style)
|| provider_definitions
.values()
.any(|definition| quoted_serialization::value_references(definition, style))
{
provider_definitions.insert(
quoted_serialization::definition_name(style).to_string(),
quoted_serialization::definition_schema(style),
);
}
}
drop(truthy_span);
insert_definitions_into_root(&mut root_schema, provider_definitions);
{
let _span = tracing::info_span!("apply_program_wrappers").entered();
program_wrapper::apply_program_wrapper_alternatives(
&mut root_schema,
contract_schema_signals.values_program_wrappers(),
contract_schema_signals.values_program_wrapper_exclusions(),
);
}
{
let _span = tracing::info_span!("apply_values_descriptions").entered();
schema_tree::apply_values_descriptions(&mut root_schema, values_descriptions);
}
drop(fill_span);
root_schema
}
pub(crate) use helm_schema_core::split_value_path;
fn common_prefix_len(left: &[String], right: &[String]) -> usize {
left.iter()
.zip(right.iter())
.take_while(|(left, right)| left == right)
.count()
}
#[cfg(test)]
#[path = "tests/mod.rs"]
mod tests;