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;
#[derive(Debug)]
pub struct LocalSchemaProvider {
root_dir: PathBuf,
allow_api_version_guess: bool,
docs: SourceDocCache<ResourceDocKey>,
}
impl LocalSchemaProvider {
#[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(),
}
}
#[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);
for c in &mut out {
c.source = InferenceSource::Shortlist;
}
out
}
}
#[cfg(test)]
#[path = "tests/provider.rs"]
mod tests;