helm-schema-k8s 0.0.4

Generate an accurate JSON schema for any helm chart
Documentation
use std::path::PathBuf;

use helm_schema_core::{ResourceRef, YamlPath};
use serde_json::Value;

use crate::cache::SourceDocCache;
use crate::doc_backed_schema::{LocalSchemaLeaf, lookup_root_metadata_path};
use crate::inference::cache_scan::scan_crd_source_dir;
use crate::inference::{ApiVersionCandidate, InferenceSource};
use crate::local_schema_universe::ResourceDocKey;
use crate::lookup::{
    K8sSchemaProvider, ProviderLookupResult, ProviderOrigin, ProviderSchemaSource,
};
use crate::schema_doc::SchemaDoc;

/// Provider for user-supplied schema files arranged by resource coordinate.
#[derive(Debug)]
pub struct LocalSchemaProvider {
    root_dir: PathBuf,
    allow_api_version_guess: bool,
    docs: SourceDocCache<ResourceDocKey>,
}

impl LocalSchemaProvider {
    /// Creates a provider rooted at a local override directory.
    #[must_use]
    pub fn new(root_dir: impl Into<PathBuf>) -> Self {
        Self {
            root_dir: root_dir.into(),
            allow_api_version_guess: false,
            docs: SourceDocCache::new(),
        }
    }

    /// Enables or disables API-version inference from local overrides.
    #[must_use]
    pub fn with_api_version_guess(mut self, enabled: bool) -> Self {
        self.allow_api_version_guess = enabled;
        self
    }

    fn override_file_for(&self, resource: &ResourceRef) -> Option<PathBuf> {
        Some(
            self.root_dir
                .join(crate::filename::group_relative_path_for_resource(resource)?),
        )
    }

    fn load_schema_doc_result(&self, resource: &ResourceRef) -> LocalSchemaDocLoad {
        let Some(local) = self.override_file_for(resource) else {
            return LocalSchemaDocLoad::NotOwned;
        };
        if !local.exists() {
            return LocalSchemaDocLoad::NotOwned;
        }

        let cache_key = ResourceDocKey::from_resource(resource);
        if let Some(doc) = self.docs.read(&cache_key) {
            return LocalSchemaDocLoad::Loaded(doc);
        }

        let source_path = local.display().to_string();
        let bytes = match std::fs::read(&local) {
            Ok(bytes) => bytes,
            Err(err) => {
                return LocalSchemaDocLoad::Error {
                    source_path,
                    io_error: err.to_string(),
                };
            }
        };
        let doc = match serde_json::from_slice::<Value>(&bytes) {
            Ok(doc) => SchemaDoc::new(doc),
            Err(err) => {
                return LocalSchemaDocLoad::Error {
                    source_path,
                    io_error: err.to_string(),
                };
            }
        };
        self.docs.write(cache_key, doc.clone());
        LocalSchemaDocLoad::Loaded(doc)
    }

    fn source_for_leaf(
        &self,
        resource: &ResourceRef,
        leaf: &LocalSchemaLeaf,
    ) -> Option<ProviderSchemaSource> {
        let pointer = leaf.pointer()?;
        Some(ProviderSchemaSource::new(
            ProviderOrigin::LocalOverride,
            self.root_dir.display().to_string(),
            None,
            crate::filename::group_relative_path_for_resource(resource)?,
            pointer.to_string(),
        ))
    }
}

enum LocalSchemaDocLoad {
    Loaded(SchemaDoc),
    NotOwned,
    Error {
        source_path: String,
        io_error: String,
    },
}

impl K8sSchemaProvider for LocalSchemaProvider {
    fn origin(&self) -> ProviderOrigin {
        ProviderOrigin::LocalOverride
    }

    #[tracing::instrument(skip_all, fields(kind = resource.kind.as_str(), api_version = resource.api_version.as_str(), path_len = path.0.len()))]
    fn lookup(&self, resource: &ResourceRef, path: &YamlPath) -> ProviderLookupResult {
        match self.load_schema_doc_result(resource) {
            LocalSchemaDocLoad::Loaded(root) => {
                lookup_root_metadata_path(&root, path, |leaf| self.source_for_leaf(resource, leaf))
            }
            LocalSchemaDocLoad::NotOwned => ProviderLookupResult::NotOwned,
            LocalSchemaDocLoad::Error {
                source_path,
                io_error,
            } => ProviderLookupResult::ResourceDocMissing {
                io_error,
                source_path,
            },
        }
    }

    fn has_resource(&self, resource: &ResourceRef) -> bool {
        self.override_file_for(resource).is_some_and(|p| p.exists())
    }

    fn infer_api_version_candidates(&self, kind: &str) -> Vec<ApiVersionCandidate> {
        if !self.allow_api_version_guess {
            return Vec::new();
        }
        let kind_lc = kind.to_ascii_lowercase();
        let mut out = scan_crd_source_dir(&self.root_dir, &kind_lc, ProviderOrigin::LocalOverride);
        // Override-as-shortlist: stamp source=Shortlist if found locally.
        for c in &mut out {
            c.source = InferenceSource::Shortlist;
        }
        out
    }
}

#[cfg(test)]
#[path = "tests/provider.rs"]
mod tests;