Skip to main content

helm_schema_gen/
lib.rs

1//! JSON Schema lowering from normalized Helm contract signals.
2
3mod base_schema;
4#[cfg(feature = "bench-support")]
5pub mod bench_support;
6mod condition_encoding;
7mod emission_plan;
8mod emission_policy;
9mod emission_report;
10mod foreign_schema;
11mod merge;
12mod overlay_lowering;
13mod path_resolver;
14mod path_schema;
15mod program_wrapper;
16mod provider_definitions;
17mod provider_requirement_synthesis;
18mod provider_schema;
19mod quoted_serialization;
20pub mod required_inference;
21mod requirement_domain;
22mod resolve_policy;
23mod schema_model;
24mod schema_node;
25mod schema_tree;
26mod values_yaml;
27
28use std::collections::{BTreeMap, BTreeSet};
29
30use helm_schema_core::{ContractSchemaSignals, ResourceSchemaOracle};
31use serde_json::Value;
32use serde_yaml::Value as YamlValue;
33
34pub(crate) use emission_plan::CompletionPass;
35use emission_plan::LoweredEmissionPlan;
36pub use emission_policy::{
37    ConditionalAnchors, EmissionClassKind, EmissionOrigin, EmissionPolicy, EmissionPolicyDelta,
38    EmissionSelection, InvalidEmissionPolicy, POLICY_VOCABULARY_VERSION, ResolvedEmissionPolicy,
39    SchemaProfile,
40};
41pub use emission_report::{
42    CanonicalizationCounts, CarrierCounts, EmissionReport, FactCounts, InsertionAbstentionCounts,
43    MandatoryOutcomes,
44};
45
46/// Parsed values documents consumed together during schema lowering.
47#[derive(Debug, Clone, PartialEq)]
48pub struct PreparedValuesDocuments {
49    composed: YamlValue,
50    dependency: YamlValue,
51    dependency_refill: YamlValue,
52}
53
54impl PreparedValuesDocuments {
55    /// Creates the complete values-document bundle prepared by the caller.
56    #[must_use]
57    pub fn new(composed: YamlValue, dependency: YamlValue, dependency_refill: YamlValue) -> Self {
58        Self {
59            composed,
60            dependency,
61            dependency_refill,
62        }
63    }
64}
65
66/// Inputs for JSON Schema generation from the current contract schema signals.
67///
68/// The generated schema is derived from the contract-layer signal bundle plus
69/// optional structural signals collected by earlier analysis phases.
70/// Values-file descriptions are metadata only: they are applied only to schema
71/// nodes that already exist from template or values evidence.
72#[derive(Clone, Copy)]
73pub struct ValuesSchemaInput<'a> {
74    /// Path-local static-analysis facts prepared by contract finalization.
75    pub contract_schema_signals: &'a ContractSchemaSignals,
76    /// Resource-schema oracle used to constrain rendered Kubernetes fields.
77    pub provider: &'a dyn ResourceSchemaOracle,
78    /// Parsed chart and dependency values documents, when available.
79    ///
80    /// The dependency document contains only dependency charts' declared
81    /// defaults, composed under their value prefixes. A key present there
82    /// fills at the SUBCHART's
83    /// coalesce stage even when the parent-level document misses it —
84    /// including after a parent-level null-deletion — so absence at such
85    /// paths reads as the subchart default instead of nil. When absent,
86    /// every missing key reads as nil.
87    ///
88    /// The dependency-refill document contains the same defaults without the
89    /// parent-declared subtraction: what Helm refills a missing or null
90    /// dependency values root with. Absence below such a root reads as nil
91    /// only while the root survives — deleting the root itself hands the
92    /// whole subtree back to the subchart's own defaults, and only the keys
93    /// they miss stay gone.
94    pub values_documents: Option<&'a PreparedValuesDocuments>,
95    /// Descendant `global.*` input paths hidden by an ancestor chart's
96    /// declared global value. Helm accepts these paths but never exposes
97    /// them to the descendant consumer.
98    pub shadowed_input_paths: Option<&'a BTreeSet<String>>,
99    /// Documentation strings keyed by canonical values path.
100    pub values_descriptions: Option<&'a BTreeMap<String, String>>,
101    /// Complete valid policy selecting analyzed contract evidence.
102    pub emission_policy: EmissionPolicy,
103}
104
105impl<'a> ValuesSchemaInput<'a> {
106    /// Creates schema input with contract signals and a resource provider.
107    pub fn new(
108        contract_schema_signals: &'a ContractSchemaSignals,
109        provider: &'a dyn ResourceSchemaOracle,
110    ) -> Self {
111        Self {
112            contract_schema_signals,
113            provider,
114            values_documents: None,
115            shadowed_input_paths: None,
116            values_descriptions: None,
117            emission_policy: SchemaProfile::Full.resolved_policy().policy(),
118        }
119    }
120
121    /// Attaches the parsed chart and dependency values documents.
122    #[must_use]
123    pub fn with_values_documents(mut self, values_documents: &'a PreparedValuesDocuments) -> Self {
124        self.values_documents = Some(values_documents);
125        self
126    }
127
128    /// Marks accepted values paths that Helm shadows before template evaluation.
129    #[must_use]
130    pub fn with_shadowed_input_paths(mut self, shadowed_input_paths: &'a BTreeSet<String>) -> Self {
131        self.shadowed_input_paths = Some(shadowed_input_paths);
132        self
133    }
134
135    /// Attaches values-file descriptions as output metadata.
136    #[must_use]
137    pub fn with_values_descriptions(
138        mut self,
139        values_descriptions: &'a BTreeMap<String, String>,
140    ) -> Self {
141        self.values_descriptions = Some(values_descriptions);
142        self
143    }
144
145    /// Selects the schema emission profile.
146    #[must_use]
147    pub fn with_profile(mut self, profile: SchemaProfile) -> Self {
148        self.emission_policy = profile.resolved_policy().policy();
149        self
150    }
151
152    /// Selects an already validated emission policy.
153    #[must_use]
154    pub fn with_emission_policy(mut self, policy: EmissionPolicy) -> Self {
155        self.emission_policy = policy;
156        self
157    }
158}
159
160/// Generate a JSON Schema with chart-authored values-file descriptions.
161///
162/// The output schema has no `required` arrays inferred by helm-schema; callers
163/// that want that behaviour layer [`required_inference::apply_required_inference`]
164/// on top of the returned schema. Keeping required-inference outside this
165/// function isolates a heuristic feature from the core schema-generation
166/// pipeline.
167#[tracing::instrument(skip_all)]
168pub fn generate_values_schema(input: ValuesSchemaInput<'_>) -> Value {
169    generate_values_schema_with_report(input).0
170}
171
172/// Generates a JSON Schema and the fact-level accounting from the same emitter run.
173///
174/// The report describes generator emission before caller-owned overrides and
175/// output-pipeline transforms.
176#[tracing::instrument(skip_all)]
177pub fn generate_values_schema_with_report(input: ValuesSchemaInput<'_>) -> (Value, EmissionReport) {
178    generate_values_schema_through(&input, CompletionPass::Descriptions)
179}
180
181fn generate_values_schema_through(
182    input: &ValuesSchemaInput<'_>,
183    completion_pass: CompletionPass,
184) -> (Value, EmissionReport) {
185    let plan = LoweredEmissionPlan::build(input);
186    let projected = plan.project(input.emission_policy);
187    let completed = plan.complete(projected, completion_pass);
188    (completed.schema, completed.emission_report)
189}
190
191/// The domain Go's `range` iterates without aborting: collections and nil
192/// render; integer counts iterate through Helm's `--set` int64 channel
193/// (JSON Schema cannot separate that from the failing values-file float64
194/// spelling, so the renderable channel wins) unless the loop body reads
195/// member structure integers cannot provide; strings and non-integral
196/// numbers fail in every channel.
197pub(crate) fn runtime_iterable_schema(allow_integer: bool) -> serde_json::Value {
198    let mut types = vec!["array", "object"];
199    if allow_integer {
200        types.push("integer");
201    }
202    types.push("null");
203    crate::schema_model::type_union_schema(types)
204}
205
206pub(crate) use helm_schema_core::split_value_path;
207
208fn common_prefix_len(left: &[String], right: &[String]) -> usize {
209    left.iter()
210        .zip(right.iter())
211        .take_while(|(left, right)| left == right)
212        .count()
213}
214
215#[cfg(test)]
216#[path = "tests/mod.rs"]
217mod tests;