use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use helm_schema_core::{
ConditionalGuard, ContractSchemaSignals, ContractUse, ContractValuePathFacts, MetadataFieldKind,
};
use helm_schema_gen::{
PreparedValuesDocuments, ValuesSchemaInput, generate_values_schema_with_report,
};
use helm_schema_ir::{ContractDocument, ContractIr, FinalizedContract};
use helm_schema_k8s::{Diagnostic, DiagnosticSink, LocalSchemaUniverse};
use serde_json::Value;
use crate::analysis::analyze_charts;
use crate::chart;
use crate::error::EngineResult;
use crate::generation::{GenerateOptions, GeneratedSchema, ResolvedContract};
use crate::output_pipeline::{
EmitRequest, FinalOutputPolicy, PolicyInputOptions, PreparedEmitRequest,
apply_schema_output_pipeline, load_emit_request, prepare_emit_request,
};
use crate::provider_builder;
use crate::values_roots;
#[derive(Debug, Clone)]
pub struct Analysis {
pub contract: ContractIr,
pub local_schemas: LocalSchemaUniverse,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ValuePathExplanation {
pub path: String,
pub exact_uses: Vec<ContractUse>,
pub descendant_uses: Vec<ContractUse>,
pub value_path_facts: Option<ContractValuePathFacts>,
pub guard_predicates: Vec<ConditionalGuard>,
pub metadata_fields: Vec<MetadataFieldKind>,
pub type_hints: Vec<Value>,
pub has_default_fallback: bool,
}
struct PreparedSession {
analysis: Analysis,
values_documents: PreparedValuesDocuments,
shadowed_input_paths: BTreeSet<String>,
explicit_value_paths: BTreeSet<String>,
values_descriptions: BTreeMap<String, String>,
}
impl PreparedSession {
fn from_generate_options(opts: &GenerateOptions) -> EngineResult<Self> {
let charts = &chart::discover_chart_contexts(&opts.chart_dir)?;
chart::reject_legacy_boolean_alias_keys(charts, &opts.values_files)?;
let defines = chart::build_define_index(charts, opts.include_tests)?;
let composed_values =
chart::build_composed_values_document(charts, opts.include_subchart_values)?;
let dependency_values_yaml = if opts.include_subchart_values {
chart::build_dependency_values_document(charts)?
} else {
serde_yaml::Value::Null
};
let dependency_refill_values_yaml = if opts.include_subchart_values {
chart::build_dependency_refill_values_document(charts)?
} else {
serde_yaml::Value::Null
};
let values_roots = values_roots::ValuesRoots::from_values_document(&composed_values);
let values_descriptions = chart::build_composed_values_descriptions(
charts,
opts.include_subchart_values,
&opts.values_files,
)?;
let kubernetes_version = primary_kubernetes_version(opts);
let chart_analysis = analyze_charts(
charts,
&defines,
opts.include_tests,
&values_roots,
kubernetes_version.as_deref(),
)?;
let shadowed_input_paths = chart_analysis.shadowed_input_paths;
Ok(Self {
analysis: Analysis {
contract: chart_analysis.contract,
local_schemas: chart_analysis.local_schema_universe,
},
values_documents: PreparedValuesDocuments::new(
composed_values,
dependency_values_yaml,
dependency_refill_values_yaml,
),
shadowed_input_paths,
explicit_value_paths: values_roots.explicit_paths,
values_descriptions,
})
}
}
pub struct AnalysisSession {
opts: GenerateOptions,
diagnostics: DiagnosticSink,
prepared: SessionCache<PreparedSession>,
finalized_contract: SessionCache<FinalizedContract>,
resolved_contract: SessionCache<ResolvedContract>,
generated_schema: SessionCache<GeneratedSchema>,
resolved_emission_policy: SessionCache<helm_schema_gen::ResolvedEmissionPolicy>,
}
struct SessionCache<T> {
value: Mutex<Option<Arc<T>>>,
}
impl<T> SessionCache<T> {
fn new() -> Self {
Self {
value: Mutex::new(None),
}
}
fn get_or_try_init(&self, init: impl FnOnce() -> EngineResult<T>) -> EngineResult<Arc<T>> {
{
let guard = self
.value
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(value) = guard.as_ref() {
return Ok(Arc::clone(value));
}
}
let value = Arc::new(init()?);
let mut guard = self
.value
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Ok(Arc::clone(guard.get_or_insert_with(|| Arc::clone(&value))))
}
}
impl AnalysisSession {
#[must_use]
pub fn new(opts: GenerateOptions) -> Self {
Self::with_diagnostics(opts, DiagnosticSink::new())
}
#[must_use]
pub fn with_diagnostics(opts: GenerateOptions, diagnostics: DiagnosticSink) -> Self {
Self {
opts,
diagnostics,
prepared: SessionCache::new(),
finalized_contract: SessionCache::new(),
resolved_contract: SessionCache::new(),
generated_schema: SessionCache::new(),
resolved_emission_policy: SessionCache::new(),
}
}
pub fn analysis(&self) -> EngineResult<Analysis> {
Ok(self.prepared()?.analysis.clone())
}
pub fn contract_schema_signals(&self) -> EngineResult<ContractSchemaSignals> {
Ok(self.finalized_contract()?.schema_signals().clone())
}
pub fn contract_document(&self) -> EngineResult<ContractDocument> {
Ok(self.finalized_contract()?.document())
}
pub fn resolved_contract(&self) -> EngineResult<ResolvedContract> {
Ok((*self.resolved()?).clone())
}
pub fn generated_schema(&self) -> EngineResult<GeneratedSchema> {
Ok((*self.generated_schema.get_or_try_init(|| {
let resolved = self.resolved()?;
let mut schema = resolved.schema.clone();
if self.opts.infer_required {
helm_schema_gen::required_inference::apply_required_inference(
&mut schema,
self.finalized_contract()?
.schema_signals()
.schema_evidence_by_value_path(),
&self.prepared()?.explicit_value_paths,
);
}
Ok(GeneratedSchema {
schema,
emission_report: resolved.emission_report.clone(),
})
})?)
.clone())
}
pub fn emit(&self, request: EmitRequest) -> EngineResult<Value> {
let generated = self.generated_schema()?;
apply_schema_output_pipeline(
generated.schema,
PreparedEmitRequest::empty(request),
self.chart_base_dir(),
FinalOutputPolicy::new(self.resolved_emission_policy()?, self.opts.infer_required),
)
}
pub fn emit_with_policy_paths(
&self,
override_paths: &[PathBuf],
policy_input_options: PolicyInputOptions,
request: EmitRequest,
) -> EngineResult<Value> {
let loaded = load_emit_request(override_paths, &policy_input_options, request)?;
let generated = self.generated_schema()?;
let prepared = prepare_emit_request(loaded, &policy_input_options, &generated.schema)?;
apply_schema_output_pipeline(
generated.schema,
prepared,
self.chart_base_dir(),
FinalOutputPolicy::new(self.resolved_emission_policy()?, self.opts.infer_required),
)
}
pub fn explain(&self, path: &str) -> EngineResult<ValuePathExplanation> {
let normalized_path = normalize_values_path(path);
let finalized_contract = self.finalized_contract()?;
let uses = finalized_contract.uses();
let schema_signals = finalized_contract.schema_signals();
let normalized_values_path = helm_schema_core::ValuesPath::parse(&normalized_path);
let evidence = schema_signals.evidence_for(&normalized_values_path);
let exact_uses = uses
.iter()
.filter(|use_| use_.source_expr == normalized_values_path)
.cloned()
.collect();
let descendant_uses = uses
.iter()
.filter(|use_| use_.source_expr.is_descendant_of(&normalized_values_path))
.cloned()
.collect();
let value_path_facts = evidence.map(|evidence| evidence.facts);
let guard_predicates = evidence
.map(|evidence| evidence.guard_predicates.clone())
.unwrap_or_default();
let metadata_fields = evidence
.map(|evidence| evidence.metadata_field_kinds.iter().copied().collect())
.unwrap_or_default();
let type_hints: Vec<serde_json::Value> = evidence
.map(|evidence| {
let schema_types = &evidence.type_hints;
schema_types
.iter()
.map(|schema_type| serde_json::json!({ "type": schema_type }))
.collect()
})
.unwrap_or_default();
let has_default_fallback =
evidence.is_some_and(|evidence| evidence.requiredness.has_default_fallback);
Ok(ValuePathExplanation {
path: normalized_path,
exact_uses,
descendant_uses,
value_path_facts,
guard_predicates,
metadata_fields,
type_hints,
has_default_fallback,
})
}
fn prepared(&self) -> EngineResult<Arc<PreparedSession>> {
self.prepared
.get_or_try_init(|| PreparedSession::from_generate_options(&self.opts))
}
fn chart_base_dir(&self) -> &Path {
Path::new(self.opts.chart_dir.as_str())
}
fn finalized_contract(&self) -> EngineResult<Arc<FinalizedContract>> {
self.finalized_contract.get_or_try_init(|| {
let prepared = self.prepared()?;
let finalized = prepared.analysis.contract.clone().finalize();
emit_input_channel_diagnostics(finalized.schema_signals(), &self.diagnostics);
Ok(finalized)
})
}
fn resolved(&self) -> EngineResult<Arc<ResolvedContract>> {
self.resolved_contract.get_or_try_init(|| {
let prepared = self.prepared()?;
let finalized_contract = self.finalized_contract()?;
let mut provider_options = self.opts.provider.clone();
provider_options.local_schema_universe = prepared.analysis.local_schemas.clone();
let provider =
provider_builder::build_provider(&provider_options, Some(&self.diagnostics));
let (schema, emission_report) = generate_values_schema_with_report(
ValuesSchemaInput::new(finalized_contract.schema_signals(), &provider)
.with_values_documents(&prepared.values_documents)
.with_shadowed_input_paths(&prepared.shadowed_input_paths)
.with_values_descriptions(&prepared.values_descriptions)
.with_emission_policy(self.resolved_emission_policy()?.policy()),
);
Ok(ResolvedContract {
schema,
emission_report,
})
})
}
fn resolved_emission_policy(&self) -> EngineResult<helm_schema_gen::ResolvedEmissionPolicy> {
Ok(*self
.resolved_emission_policy
.get_or_try_init(|| Ok(self.opts.emission.resolve()?))?)
}
#[cfg(all(feature = "bench-support", test))]
pub(crate) fn benchmark_emission_policies(
&self,
policies: &[helm_schema_gen::bench_support::BenchmarkPolicy],
runs: std::num::NonZeroUsize,
) -> EngineResult<helm_schema_gen::bench_support::MultiPolicyBenchmark> {
let prepared = self.prepared()?;
let finalized_contract = self.finalized_contract()?;
let mut provider_options = self.opts.provider.clone();
provider_options.local_schema_universe = prepared.analysis.local_schemas.clone();
let provider = provider_builder::build_provider(&provider_options, Some(&self.diagnostics));
let input = ValuesSchemaInput::new(finalized_contract.schema_signals(), &provider)
.with_values_documents(&prepared.values_documents)
.with_shadowed_input_paths(&prepared.shadowed_input_paths)
.with_values_descriptions(&prepared.values_descriptions);
Ok(helm_schema_gen::bench_support::benchmark_policies(
&input, policies, runs,
))
}
}
pub(crate) fn emit_input_channel_diagnostics(
signals: &ContractSchemaSignals,
diagnostics: &DiagnosticSink,
) {
for (value_path, evidence) in signals.schema_evidence_by_value_path() {
let base_is_ambiguous = evidence.facts.is_direct_ranged_source
&& !evidence.facts.has_destructured_range_use
&& !evidence.facts.has_json_decoded_range_use;
let guarded_is_ambiguous = evidence.conditional_overlays.iter().any(|overlay| {
overlay.evidence.facts.is_direct_ranged_source
&& !overlay.evidence.facts.has_destructured_range_use
&& !overlay.evidence.facts.has_json_decoded_range_use
});
if base_is_ambiguous || guarded_is_ambiguous {
diagnostics.push(Diagnostic::InputChannelNumericRangeAmbiguity {
value_path: value_path.encode(),
});
}
}
}
fn normalize_values_path(path: &str) -> String {
let path = path.trim();
if let Some(stripped) = path.strip_prefix(".Values.") {
stripped.to_string()
} else if path == ".Values" {
String::new()
} else {
path.to_string()
}
}
fn primary_kubernetes_version(opts: &GenerateOptions) -> Option<String> {
let token = opts.provider.k8s_versions.first()?;
let token = token.trim().strip_prefix('v').unwrap_or(token.trim());
let core: String = token
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let parts: Vec<&str> = core.split('.').collect();
if parts.is_empty()
|| parts.len() > 3
|| parts
.iter()
.any(|part| part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()))
{
return None;
}
Some(core)
}