Skip to main content

helm_schema/output_pipeline/
format.rs

1use std::collections::BTreeSet;
2use std::io::Write;
3
4use serde_json::Value;
5
6use crate::error::EngineResult;
7use crate::output_pipeline::JsonOutputFormat;
8
9/// Helm refuses to load any chart file larger than 5 MiB, and a chart's
10/// `values.schema.json` counts against that limit.
11pub const HELM_MAX_CHART_FILE_BYTES: usize = 5 * 1024 * 1024;
12
13/// Measurements of the exact final document written for Helm to compile.
14#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
15pub struct FinalOutputMetrics {
16    /// Bytes written, including the trailing newline.
17    pub serialized_bytes: usize,
18    /// JSON object nodes in the final document.
19    pub objects: usize,
20    /// JSON Schema `if` nodes in the final document.
21    pub condition_nodes: usize,
22    /// Distinct serialized `if` payloads.
23    pub unique_conditions: usize,
24    /// Distinct serialized `then` payloads.
25    pub unique_then_payloads: usize,
26}
27
28/// Serializes a schema in the requested JSON format and appends a newline.
29///
30/// Pretty output automatically falls back to compact JSON before crossing
31/// Helm's per-file size limit.
32///
33/// # Errors
34///
35/// Returns an error when JSON serialization or writing to `out` fails.
36#[tracing::instrument(skip_all, fields(format = ?format))]
37pub fn write_schema_json(
38    out: &mut impl Write,
39    schema: &Value,
40    format: JsonOutputFormat,
41) -> EngineResult<FinalOutputMetrics> {
42    let mut bytes = match format {
43        JsonOutputFormat::Compact => serde_json::to_vec(schema)?,
44        JsonOutputFormat::Pretty => {
45            // A schema whose pretty serialization crosses Helm's chart-file
46            // limit still fits comfortably in compact form (whitespace is
47            // most of the size at that scale), so pretty degrades to
48            // compact rather than emitting a schema the chart cannot ship.
49            let pretty = serde_json::to_vec_pretty(schema)?;
50            if pretty.len() >= HELM_MAX_CHART_FILE_BYTES {
51                serde_json::to_vec(schema)?
52            } else {
53                pretty
54            }
55        }
56    };
57    bytes.push(b'\n');
58    out.write_all(&bytes)?;
59    Ok(final_output_metrics(schema, bytes.len()))
60}
61
62fn final_output_metrics(schema: &Value, serialized_bytes: usize) -> FinalOutputMetrics {
63    fn visit(
64        value: &Value,
65        metrics: &mut FinalOutputMetrics,
66        conditions: &mut BTreeSet<String>,
67        then_payloads: &mut BTreeSet<String>,
68    ) {
69        match value {
70            Value::Object(object) => {
71                metrics.objects += 1;
72                if let Some(condition) = object.get("if") {
73                    metrics.condition_nodes += 1;
74                    conditions.insert(helm_schema_json_schema_walk::canonical_json_string(
75                        condition,
76                    ));
77                }
78                if let Some(then_payload) = object.get("then") {
79                    then_payloads.insert(helm_schema_json_schema_walk::canonical_json_string(
80                        then_payload,
81                    ));
82                }
83                for child in object.values() {
84                    visit(child, metrics, conditions, then_payloads);
85                }
86            }
87            Value::Array(items) => {
88                for item in items {
89                    visit(item, metrics, conditions, then_payloads);
90                }
91            }
92            Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
93        }
94    }
95
96    let mut metrics = FinalOutputMetrics {
97        serialized_bytes,
98        ..FinalOutputMetrics::default()
99    };
100    let mut conditions = BTreeSet::new();
101    let mut then_payloads = BTreeSet::new();
102    visit(schema, &mut metrics, &mut conditions, &mut then_payloads);
103    metrics.unique_conditions = conditions.len();
104    metrics.unique_then_payloads = then_payloads.len();
105    metrics
106}
107
108#[cfg(test)]
109#[path = "tests/format.rs"]
110mod tests;