use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::error::EngineResult;
use crate::flatten;
use crate::load_budget::read_to_end_capped;
use crate::output_pipeline::{EmitRequest, PolicyInputOptions, ReferencePolicy};
use crate::schema_override::{PreparedOverride, UnpreparedOverride};
#[derive(Debug)]
pub(crate) struct LoadedEmitRequest {
loaded_overrides: Vec<LoadedOverride>,
request: EmitRequest,
}
#[derive(Debug)]
struct LoadedOverride {
path: PathBuf,
schema: UnpreparedOverride,
}
#[derive(Debug)]
pub(crate) struct PreparedEmitRequest {
prepared_override_schemas: Vec<PreparedOverride>,
pub(super) request: EmitRequest,
}
pub(super) struct PreparedOverridesIdentity {
pub(super) count: usize,
pub(super) digest: String,
}
impl PreparedEmitRequest {
pub(crate) fn empty(request: EmitRequest) -> Self {
Self {
prepared_override_schemas: Vec::new(),
request,
}
}
pub(super) fn override_count(&self) -> usize {
self.prepared_override_schemas.len()
}
pub(super) fn identity(&self) -> PreparedOverridesIdentity {
let identities = Value::Array(
self.prepared_override_schemas
.iter()
.map(PreparedOverride::identity)
.collect(),
);
let canonical = helm_schema_json_schema_walk::canonical_json_string(&identities);
let digest = Sha256::digest(canonical.as_bytes());
let mut digest_hex = String::with_capacity(digest.len() * 2);
for byte in digest {
let _ = write!(digest_hex, "{byte:02x}");
}
PreparedOverridesIdentity {
count: self.prepared_override_schemas.len(),
digest: digest_hex,
}
}
pub(super) fn into_prepared_override_schemas(self) -> Vec<PreparedOverride> {
self.prepared_override_schemas
}
}
#[tracing::instrument(skip_all, fields(override_count = paths.len()))]
pub(crate) fn load_emit_request(
paths: &[PathBuf],
options: &PolicyInputOptions,
request: EmitRequest,
) -> EngineResult<LoadedEmitRequest> {
let loaded_overrides = paths
.iter()
.map(|path| {
Ok(LoadedOverride {
path: path.clone(),
schema: load_override_schema(path, options)?,
})
})
.collect::<EngineResult<Vec<_>>>()?;
Ok(LoadedEmitRequest {
loaded_overrides,
request,
})
}
#[tracing::instrument(skip_all, fields(override_count = loaded.loaded_overrides.len()))]
pub(crate) fn prepare_emit_request(
loaded: LoadedEmitRequest,
options: &PolicyInputOptions,
base_schema: &Value,
) -> EngineResult<PreparedEmitRequest> {
let LoadedEmitRequest {
loaded_overrides,
request,
} = loaded;
let mut namespace = flatten::BundleNamespace::default();
namespace.reserve_schema(base_schema);
for loaded in &loaded_overrides {
namespace.reserve_schema(loaded.schema.schema());
}
let prepared_override_schemas = loaded_overrides
.into_iter()
.map(|loaded| {
let prepared_schema = prepare_override_schema(
loaded.schema.schema(),
&loaded.path,
options,
request.reference_policy,
&mut namespace,
)?;
Ok(loaded.schema.into_prepared(prepared_schema))
})
.collect::<EngineResult<Vec<_>>>()?;
Ok(PreparedEmitRequest {
prepared_override_schemas,
request,
})
}
#[tracing::instrument(skip_all)]
fn load_override_schema(
path: &Path,
options: &PolicyInputOptions,
) -> EngineResult<UnpreparedOverride> {
let override_schema = load_json_file(path, options.load_budget.max_schema_document_bytes)?;
if !override_schema.is_object() && !override_schema.is_boolean() {
return Err(crate::error::CliError::InvalidOverrideRoot {
path: path.to_path_buf(),
kind: json_kind(&override_schema),
});
}
Ok(UnpreparedOverride::capture(override_schema))
}
#[tracing::instrument(skip_all, fields(reference_policy = ?reference_policy))]
fn prepare_override_schema(
schema: &Value,
override_path: &Path,
options: &PolicyInputOptions,
reference_policy: ReferencePolicy,
namespace: &mut flatten::BundleNamespace,
) -> EngineResult<Value> {
let override_base = override_path.parent().unwrap_or_else(|| Path::new("."));
match reference_policy {
ReferencePolicy::SelfContained => flatten::bundle_refs_in_namespace(
schema.clone(),
override_base,
options.fetch_policy,
options.load_budget,
namespace,
),
ReferencePolicy::FullyInlinedExport => flatten::flatten_refs(
schema,
override_base,
options.fetch_policy,
options.load_budget,
),
ReferencePolicy::PreserveRefs => Ok(schema.clone()),
}
}
fn json_kind(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn load_json_file(path: &Path, max_bytes: usize) -> EngineResult<Value> {
let mut file = std::fs::File::open(path)?;
let bytes = read_to_end_capped(&mut file, max_bytes, path.display().to_string())?;
let value: Value = serde_json::from_slice(&bytes)?;
Ok(value)
}
#[cfg(test)]
#[path = "tests/overrides.rs"]
mod tests;