1mod base_schema;
4mod condition_encoding;
5mod foreign_schema;
6mod merge;
7mod overlay_lowering;
8mod path_resolver;
9mod path_schema;
10mod program_wrapper;
11mod provider_definitions;
12mod provider_schema;
13mod quoted_serialization;
14pub mod required_inference;
15mod required_source_backprojection;
16mod resolve_policy;
17mod schema_model;
18mod schema_node;
19mod schema_tree;
20mod values_yaml;
21
22use std::collections::{BTreeMap, BTreeSet};
23
24use helm_schema_core::{ContractSchemaSignals, ResourceSchemaOracle};
25use serde_json::Value;
26use serde_yaml::Value as YamlValue;
27
28use base_schema::{ConditionalTargetIndex, classify_base};
29use condition_encoding::{
30 HELM_TRUTHY_DEFINITION_NAME, helm_truthy_definition_schema, value_references_helm_truthy,
31};
32use overlay_lowering::{
33 append_conditional_schemas, append_terminal_clauses, collect_conditional_schemas,
34};
35use path_resolver::PathSchemaResolver;
36use provider_definitions::{
37 extract_provider_definitions, extract_repeated_provider_payloads, insert_definitions_into_root,
38};
39use schema_tree::{SchemaDocument, draft07_root_document};
40
41#[derive(Clone, Copy)]
48pub struct ValuesSchemaInput<'a> {
49 pub contract_schema_signals: &'a ContractSchemaSignals,
51 pub provider: &'a dyn ResourceSchemaOracle,
53 pub values_yaml: Option<&'a str>,
55 pub dependency_values_yaml: Option<&'a str>,
62 pub values_descriptions: Option<&'a BTreeMap<String, String>>,
64}
65
66impl<'a> ValuesSchemaInput<'a> {
67 pub fn new(
69 contract_schema_signals: &'a ContractSchemaSignals,
70 provider: &'a dyn ResourceSchemaOracle,
71 ) -> Self {
72 Self {
73 contract_schema_signals,
74 provider,
75 values_yaml: None,
76 dependency_values_yaml: None,
77 values_descriptions: None,
78 }
79 }
80
81 #[must_use]
83 pub fn with_values_yaml(mut self, values_yaml: Option<&'a str>) -> Self {
84 self.values_yaml = values_yaml;
85 self
86 }
87
88 #[must_use]
90 pub fn with_dependency_values_yaml(mut self, dependency_values_yaml: Option<&'a str>) -> Self {
91 self.dependency_values_yaml = dependency_values_yaml;
92 self
93 }
94
95 #[must_use]
97 pub fn with_values_descriptions(
98 mut self,
99 values_descriptions: &'a BTreeMap<String, String>,
100 ) -> Self {
101 self.values_descriptions = Some(values_descriptions);
102 self
103 }
104}
105
106#[tracing::instrument(skip_all)]
114pub fn generate_values_schema(input: ValuesSchemaInput<'_>) -> Value {
115 let empty_values_descriptions = BTreeMap::new();
116 let values_descriptions = input
117 .values_descriptions
118 .unwrap_or(&empty_values_descriptions);
119
120 let mut values_yaml_doc = input
121 .values_yaml
122 .and_then(|s| serde_yaml::from_str::<YamlValue>(s).ok())
123 .unwrap_or(YamlValue::Null);
124 values_yaml::apply_values_default_sources(
125 &mut values_yaml_doc,
126 input.contract_schema_signals.values_default_sources(),
127 );
128 let mut subchart_defaults_doc = input
129 .dependency_values_yaml
130 .and_then(|s| serde_yaml::from_str::<YamlValue>(s).ok())
131 .unwrap_or(YamlValue::Null);
132 values_yaml::copy_values_default_sources(
137 &mut subchart_defaults_doc,
138 &values_yaml_doc,
139 input.contract_schema_signals.values_default_sources(),
140 );
141
142 let root_schema = build_root_schema(
143 input.contract_schema_signals,
144 &values_yaml_doc,
145 &subchart_defaults_doc,
146 values_descriptions,
147 input.provider,
148 );
149
150 draft07_root_document(root_schema)
151}
152
153pub(crate) fn runtime_iterable_schema(allow_integer: bool) -> serde_json::Value {
160 let mut arms = vec![
161 serde_json::json!({ "type": "array" }),
162 serde_json::json!({ "type": "object" }),
163 ];
164 if allow_integer {
165 arms.push(serde_json::json!({ "type": "integer" }));
166 }
167 arms.push(serde_json::json!({ "type": "null" }));
168 serde_json::json!({ "anyOf": arms })
169}
170
171#[tracing::instrument(skip_all)]
172#[expect(
173 clippy::too_many_lines,
174 reason = "keeping this semantic lowering operation together makes its state transitions easier to audit"
175)]
176fn build_root_schema(
177 contract_schema_signals: &ContractSchemaSignals,
178 values_yaml_doc: &YamlValue,
179 subchart_defaults_doc: &YamlValue,
180 values_descriptions: &BTreeMap<String, String>,
181 provider: &dyn ResourceSchemaOracle,
182) -> Value {
183 let mut root_schema = SchemaDocument::new_root_object();
184 let path_resolver = PathSchemaResolver::new(contract_schema_signals, values_yaml_doc, provider);
185 let mut resolved_paths = path_resolver.resolve_all();
186 let mut conditional_schemas = collect_conditional_schemas(
187 &resolved_paths,
188 contract_schema_signals,
189 values_yaml_doc,
190 provider,
191 );
192 let provider_definitions = extract_provider_definitions(
193 &mut resolved_paths,
194 &mut conditional_schemas,
195 values_descriptions,
196 );
197 let conditional_targets = ConditionalTargetIndex::from_conditionals(&conditional_schemas);
198 let accepted_values_root_paths = contract_schema_signals
199 .schema_evidence_by_value_path()
200 .values()
201 .filter(|evidence| evidence.facts.accepted_values_root_fragment)
202 .map(|evidence| split_value_path(&evidence.value_path))
203 .collect::<Vec<_>>();
204 let no_owning_ancestors = BTreeSet::new();
205 let base_span = tracing::info_span!("base_path_insertion").entered();
206 let owning_paths = resolved_paths
207 .iter()
208 .filter(|resolved_path| {
209 classify_base(resolved_path, &conditional_targets, &no_owning_ancestors)
210 .owns_descendants()
211 })
212 .map(|resolved_path| resolved_path.path_segments.clone())
213 .collect::<BTreeSet<_>>();
214 for resolved_path in &resolved_paths {
215 let owner = classify_base(resolved_path, &conditional_targets, &owning_paths);
216 let Some(schema) = owner.schema(resolved_path) else {
217 continue;
218 };
219 if owner.replaces() {
220 root_schema.replace_path_schema(&resolved_path.path_segments, schema);
221 } else {
222 root_schema.insert_path_schema(&resolved_path.path_segments, schema);
223 }
224 }
225 drop(base_span);
226 append_conditional_schemas(
227 &mut root_schema,
228 conditional_schemas,
229 values_yaml_doc,
230 subchart_defaults_doc,
231 );
232 append_terminal_clauses(
233 &mut root_schema,
234 contract_schema_signals.terminal_clauses(),
235 values_yaml_doc,
236 subchart_defaults_doc,
237 );
238 let mut default_fill_skip_paths = conditional_targets.target_paths.clone();
242 for resolved_path in &resolved_paths {
243 if resolved_path.used_as_serialized {
244 default_fill_skip_paths.insert(resolved_path.path_segments.clone());
245 }
246 }
247 for (value_path, evidence) in contract_schema_signals.schema_evidence_by_value_path() {
248 if evidence.facts.used_as_yaml_serialized {
249 default_fill_skip_paths.insert(split_value_path(value_path));
250 }
251 }
252 for value_path in contract_schema_signals.direct_ranged_value_paths() {
255 default_fill_skip_paths.insert(split_value_path(value_path));
256 }
257 let fill_span = tracing::info_span!("default_fill_and_finish").entered();
258 {
259 let _span = tracing::info_span!("merge_missing_defaults").entered();
260 root_schema.merge_missing_values_yaml_defaults_under_roots(
261 values_yaml_doc,
262 &accepted_values_root_paths,
263 &default_fill_skip_paths,
264 );
265 }
266 root_schema.open_helm_global_namespace();
267
268 let mut root_schema = root_schema.into_value();
269 if let Ok(declared_defaults) = serde_json::to_value(values_yaml_doc)
270 && declared_defaults.is_object()
271 {
272 let _span = tracing::info_span!("preserve_declared_defaults").entered();
273 root_schema =
274 resolve_policy::preserve_declared_default_in_schema(root_schema, &declared_defaults);
275 }
276 let mut provider_definitions = provider_definitions;
277 {
278 let _span = tracing::info_span!("extract_repeated_provider_payloads").entered();
279 provider_definitions.extend(extract_repeated_provider_payloads(&mut root_schema));
280 }
281 let truthy_span = tracing::info_span!("helm_truthy_scan").entered();
282 if value_references_helm_truthy(&root_schema)
283 || provider_definitions
284 .values()
285 .any(value_references_helm_truthy)
286 {
287 provider_definitions.insert(
288 HELM_TRUTHY_DEFINITION_NAME.to_string(),
289 helm_truthy_definition_schema(),
290 );
291 }
292 for style in [
293 helm_schema_core::QuotedScalarStyle::Double,
294 helm_schema_core::QuotedScalarStyle::Single,
295 ] {
296 if quoted_serialization::value_references(&root_schema, style)
297 || provider_definitions
298 .values()
299 .any(|definition| quoted_serialization::value_references(definition, style))
300 {
301 provider_definitions.insert(
302 quoted_serialization::definition_name(style).to_string(),
303 quoted_serialization::definition_schema(style),
304 );
305 }
306 }
307 drop(truthy_span);
308 insert_definitions_into_root(&mut root_schema, provider_definitions);
309 {
310 let _span = tracing::info_span!("apply_program_wrappers").entered();
311 program_wrapper::apply_program_wrapper_alternatives(
312 &mut root_schema,
313 contract_schema_signals.values_program_wrappers(),
314 contract_schema_signals.values_program_wrapper_exclusions(),
315 );
316 }
317 {
318 let _span = tracing::info_span!("apply_values_descriptions").entered();
319 schema_tree::apply_values_descriptions(&mut root_schema, values_descriptions);
320 }
321 drop(fill_span);
322 root_schema
323}
324
325pub(crate) use helm_schema_core::split_value_path;
326
327fn common_prefix_len(left: &[String], right: &[String]) -> usize {
328 left.iter()
329 .zip(right.iter())
330 .take_while(|(left, right)| left == right)
331 .count()
332}
333
334#[cfg(test)]
335#[path = "tests/mod.rs"]
336mod tests;