helm-schema-core 0.0.6

Generate an accurate JSON schema for any helm chart
Documentation
use serde::{Deserialize, Serialize};

use crate::{ContractProvenance, Guard, GuardDnf, ResourceRef, ValueKind, YamlPath};

/// The rendered text is ONE SEGMENT of the source string split by a literal
/// separator (`regexSplit ":" . -1 | last` extracting a port suffix): the
/// sink schema constrains that segment, never the whole raw value.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SplitSegmentUse {
    /// Literal delimiter used to split the source string.
    pub separator: String,
    /// The LAST segment when true, the first otherwise.
    pub last: bool,
}

/// A structural transform applied to one ordered merge layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum MergeLayerTransform {
    /// The layer reaches the merge unchanged.
    Identity,
    /// Nil members are recursively removed before the layer reaches the merge.
    NilScrubbed,
    /// Helm's map-only YAML decoder discards non-mapping source shapes.
    ParsedMap,
}

/// The value is one layer of an ordered Sprig `merge`: a key of an earlier
/// layer shadows the same key of every later layer at the rendered sink, so
/// a later layer's member reaches the sink only where every earlier layer
/// lacks that member.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MergeLayersUse {
    /// Every layer's values path, highest precedence first.
    pub layers: Vec<String>,
    /// This use's own index within `layers`.
    pub position: usize,
    /// Per-layer transforms, parallel to `layers`.
    pub transforms: Vec<MergeLayerTransform>,
    /// Whether the layer facts came from a local binding's metadata rather
    /// than the render site's own layered value.
    ///
    /// Identity-only binding merges keep their ordinary branch routing
    /// because sibling dispatch arms may contribute other input shapes.
    /// Structurally transformed bindings retain layered routing so each
    /// transform's selection semantics remain visible at emission.
    pub via_binding: bool,
}

impl MergeLayersUse {
    /// The higher-precedence layer paths whose keys shadow this layer's.
    #[must_use]
    pub fn shadowed_by(&self) -> &[String] {
        self.layers
            .get(..self.position.min(self.layers.len()))
            .unwrap_or_default()
    }

    /// Returns the transform applied to this use's layer.
    #[must_use]
    pub fn own_transform(&self) -> MergeLayerTransform {
        self.transforms
            .get(self.position)
            .copied()
            .unwrap_or(MergeLayerTransform::Identity)
    }
}

/// A contract claim for one observed values path.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct ContractUse {
    /// Canonical values path or expression that supplied the rendered value.
    pub source_expr: String,
    /// Structural path of the value in the rendered YAML document.
    pub path: YamlPath,
    /// How the value contributes to the rendered YAML node.
    pub kind: ValueKind,
    /// Normalized condition under which the use renders.
    pub condition: GuardDnf,
    /// Kubernetes resource owning the rendered path, when known.
    pub resource: Option<ResourceRef>,
    /// Template locations and helper chains that produced the use.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub provenance: Vec<ContractProvenance>,
    /// Go template execution rendered the source through its `%v` spelling,
    /// so a provider slot observes text rather than the raw input shape.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub stringified: bool,
    /// Literal member keys the TEMPLATE writes beside this fragment splice
    /// in the same mapping (`- name: tmp` next to `toYaml .Values.tmpVolume`):
    /// the rendered object already has them, so a provider slot's object
    /// requiredness must not re-demand them from the user value.
    #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
    pub template_supplied_member_keys: std::collections::BTreeSet<String>,
    /// Set when the rendered text is one separator-delimited segment of the
    /// source string rather than the raw value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub split_segment: Option<SplitSegmentUse>,
    /// Set when the value renders as one layer of an ordered `merge`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub merge_layers: Option<MergeLayersUse>,
    /// Set when the rendered text is the collection's RANGE KEY rather than
    /// its value: the sink constrains the key domain only.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub range_key: bool,
    /// The rendered text is a Sprig `quote`/`squote` of the value, which
    /// skips nil operands: a missing or null source renders an explicit
    /// YAML null into the sink (see
    /// [`ProviderSchemaUse::nil_omitting`](crate::ProviderSchemaUse::nil_omitting)).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub nil_omitting: bool,
    /// Literal member keys a guard-scoped `omit` may remove from the
    /// rendered map before the sink reads it. Each key maps to the sound
    /// RETAIN guards under which the key certainly survives (the omitting
    /// arm certainly did not run); an empty guard list means the key's
    /// survival is undecidable and its sink typing must abstain entirely.
    #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
    pub omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
    /// Set when the slot renders fresh text DERIVED from the value
    /// (`include … | sha256sum` checksum annotations): the sink observes
    /// neither the value nor its serialization, so the row grants its
    /// branch serialized tolerance without claiming a path-wide
    /// serialization use.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub digest: bool,
    /// Set when the value flowed through a Sprig `merge` call as a DIRECT
    /// operand: the operand's strict map contract rides its own fail
    /// implication (keyed on the call's live gate), so this row never
    /// rejects a Helm-falsy input at the base.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub merge_operand: bool,
}

impl<'de> Deserialize<'de> for ContractUse {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct WireContractUse {
            source_expr: String,
            path: YamlPath,
            kind: ValueKind,
            condition: GuardDnf,
            resource: Option<ResourceRef>,
            #[serde(default)]
            provenance: Vec<ContractProvenance>,
            #[serde(default)]
            stringified: bool,
            #[serde(default)]
            template_supplied_member_keys: std::collections::BTreeSet<String>,
            #[serde(default)]
            split_segment: Option<SplitSegmentUse>,
            #[serde(default)]
            merge_layers: Option<MergeLayersUse>,
            #[serde(default)]
            range_key: bool,
            #[serde(default)]
            nil_omitting: bool,
            #[serde(default)]
            omitted_members: std::collections::BTreeMap<String, Vec<Guard>>,
            #[serde(default)]
            digest: bool,
            #[serde(default)]
            merge_operand: bool,
        }

        let wire = WireContractUse::deserialize(deserializer)?;
        Ok(Self {
            source_expr: wire.source_expr,
            path: wire.path,
            kind: wire.kind,
            condition: wire.condition,
            resource: wire.resource,
            provenance: wire.provenance,
            stringified: wire.stringified,
            template_supplied_member_keys: wire.template_supplied_member_keys,
            split_segment: wire.split_segment,
            merge_layers: wire.merge_layers,
            range_key: wire.range_key,
            nil_omitting: wire.nil_omitting,
            omitted_members: wire.omitted_members,
            digest: wire.digest,
            merge_operand: wire.merge_operand,
        })
    }
}

impl ContractUse {
    /// Creates a contract use from one conjunction of guards.
    #[must_use]
    pub fn new(
        source_expr: String,
        path: YamlPath,
        kind: ValueKind,
        guards: Vec<Guard>,
        resource: Option<ResourceRef>,
    ) -> Self {
        Self::with_provenances(source_expr, path, kind, guards, resource, None)
    }

    /// Creates a guarded contract use with explicit source provenance.
    pub fn with_provenances(
        source_expr: String,
        path: YamlPath,
        kind: ValueKind,
        guards: Vec<Guard>,
        resource: Option<ResourceRef>,
        provenance: impl IntoIterator<Item = ContractProvenance>,
    ) -> Self {
        let condition = GuardDnf::from_guards(guards);
        Self::with_condition_and_provenances(
            source_expr,
            path,
            kind,
            condition,
            resource,
            provenance,
        )
    }

    /// Creates a contract use from an already-normalized condition.
    pub fn with_condition_and_provenances(
        source_expr: String,
        path: YamlPath,
        kind: ValueKind,
        condition: GuardDnf,
        resource: Option<ResourceRef>,
        provenance: impl IntoIterator<Item = ContractProvenance>,
    ) -> Self {
        Self {
            source_expr,
            path,
            kind,
            condition,
            resource,
            provenance: provenance.into_iter().collect(),
            stringified: false,
            template_supplied_member_keys: std::collections::BTreeSet::new(),
            split_segment: None,
            merge_layers: None,
            range_key: false,
            nil_omitting: false,
            omitted_members: std::collections::BTreeMap::new(),
            digest: false,
            merge_operand: false,
        }
    }

    /// Sorts and deduplicates provenance without changing semantic evidence.
    pub fn canonicalize(&mut self) {
        self.provenance.sort();
        self.provenance.dedup();
    }

    /// Returns the sole guard conjunction, or an empty conjunction when not singular.
    #[must_use]
    pub fn single_guard_conjunction(&self) -> Vec<Guard> {
        self.condition
            .single_guard_conjunction()
            .unwrap_or_default()
    }

    /// Rewrites the source expression and every values path in the condition.
    pub fn map_value_paths<F>(&mut self, map: &mut F)
    where
        F: FnMut(&str) -> String,
    {
        self.source_expr = map(&self.source_expr);
        self.condition.map_value_paths(map);
        if let Some(merge) = &mut self.merge_layers {
            for layer in &mut merge.layers {
                *layer = map(layer);
            }
        }
        for retain_guards in self.omitted_members.values_mut() {
            for guard in retain_guards {
                *guard = guard.clone().map_value_paths(map);
            }
        }
    }
}