helm-schema-ir 0.0.6

Generate an accurate JSON schema for any helm chart
Documentation
use std::collections::{BTreeMap, BTreeSet};

use helm_schema_ast::{TemplateExpr, parse_action_expressions};
use helm_schema_core::{Guard, GuardValue, Predicate};
use test_util::prelude::sim_assert_eq;

use crate::abstract_value::AbstractValue;
use crate::eval_effect::EvalResult;
use crate::eval_env::EvalEnv;
use crate::expr_eval::{HelperCallValueResolver, eval_expr_with_helper_calls};
use crate::scalar_value::{ScalarValue, ScalarValueDispatch};

struct StaticResolver;

impl HelperCallValueResolver for StaticResolver {
    fn resolve_helper_call(
        &mut self,
        name: &str,
        _arg: Option<&TemplateExpr>,
    ) -> Option<EvalResult> {
        match name {
            "common.name" => Some(EvalResult::from_value(AbstractValue::ValuesPath(
                "nameOverride".to_string(),
            ))),
            "common.labels" => Some(EvalResult::from_value(AbstractValue::Dict(BTreeMap::from(
                [(
                    "app".to_string(),
                    AbstractValue::ValuesPath("labels.app".to_string()),
                )],
            )))),
            "partial.feature" => Some(EvalResult::none().with_scalar_dispatch(
                ScalarValueDispatch {
                    arms: vec![(
                        Predicate::truthy_path("feature.gate"),
                        ScalarValue::Literal(GuardValue::string("true")),
                    )],
                    complete: false,
                },
            )),
            _ => None,
        }
    }
}

fn single_expr(action: &str) -> TemplateExpr {
    let exprs = parse_action_expressions(&format!("{{{{ {action} }}}}"));
    sim_assert_eq!(have: exprs.len(), want: 1, "expected exactly one parsed expression");
    exprs.into_iter().next().expect("expression exists")
}

fn eval(action: &str) -> Option<AbstractValue> {
    let mut resolver = StaticResolver;
    eval_expr_with_helper_calls(&single_expr(action), &EvalEnv::default(), &mut resolver).value
}

#[test]
fn dict_value_can_be_nested_helper_call() {
    sim_assert_eq!(
        have: eval(r#"dict "name" (include "common.name" .)"#),
        want: Some(AbstractValue::Dict(BTreeMap::from([(
            "name".to_string(),
            AbstractValue::ValuesPath("nameOverride".to_string()),
        )])))
    );
}

#[test]
fn printf_preserves_nested_helper_provenance_path() {
    sim_assert_eq!(
        have: eval(r#"printf "%s-sfx" (include "common.name" .)"#),
        want: Some(AbstractValue::ValuesPath("nameOverride".to_string()))
    );
}

#[test]
fn pipeline_merge_can_consume_nested_helper_call() {
    sim_assert_eq!(
        have: eval(r#"dict "base" "static" | merge (include "common.labels" .)"#),
        want: Some(AbstractValue::Dict(BTreeMap::from([
            (
                "app".to_string(),
                AbstractValue::ValuesPath("labels.app".to_string()),
            ),
            (
                "base".to_string(),
                AbstractValue::StringSet(["static".to_string()].into_iter().collect()),
            ),
        ])))
    );
}

#[test]
fn integer_index_on_values_path_uses_array_item_wildcard_with_helper_context() {
    sim_assert_eq!(
        have: eval(r#"dict "value" (index .Values.items 0) "name" (include "common.name" .)"#),
        want: Some(AbstractValue::Dict(BTreeMap::from([
            (
                "name".to_string(),
                AbstractValue::ValuesPath("nameOverride".to_string()),
            ),
            (
                "value".to_string(),
                AbstractValue::ValuesPath("items.*".to_string()),
            ),
        ])))
    );
}

#[test]
fn partial_helper_truth_marks_later_short_circuit_member_access_incomplete() {
    let mut resolver = StaticResolver;
    let result = eval_expr_with_helper_calls(
        &single_expr(r#"and (eq (include "partial.feature" .) "true") .Values.host.member"#),
        &EvalEnv::default(),
        &mut resolver,
    );
    let member_capture = result
        .effects
        .observed_facts
        .captures
        .iter()
        .find(|capture| {
            matches!(
                capture.kind,
                crate::eval_effect::CaptureKind::MemberAccess { .. }
            )
        })
        .map(|capture| capture.conjunction.iter().cloned().collect::<BTreeSet<_>>());

    sim_assert_eq!(
        have: member_capture,
        want: Some(BTreeSet::from([
            Predicate::approximate_with_sound_subset(
                "and operand execution",
                BTreeSet::from(["feature.gate".to_string()]),
                vec![Guard::Truthy {
                    path: "feature.gate".to_string(),
                }],
            ),
            Predicate::from(Guard::TypeIs {
                path: "host".to_string(),
                schema_type: "object".to_string(),
            })
            .negated(),
        ])),
    );
    sim_assert_eq!(have: result.truth.predicate(), want: None);
    sim_assert_eq!(
        have: result.truth.when_true(),
        want: Predicate::all(vec![
            Predicate::truthy_path("feature.gate"),
            Predicate::truthy_path("host.member"),
        ]),
    );
}