use alloc::collections::BTreeSet;
use lintel_schema_cache::SchemaCache;
use schema_catalog::FileFormat;
use tracing::error;
use super::PostprocessContext;
use crate::download::{LintelExtra, parse_lintel_extra};
pub(super) fn inject_lintel(ctx: &PostprocessContext<'_>, value: &mut serde_json::Value) {
let parsers = if ctx.parsers.is_empty() {
parsers_from_file_match(&ctx.file_match)
} else {
ctx.parsers.clone()
};
if let Some((source, hash)) = &ctx.lintel_source {
inject_lintel_extra(
value,
LintelExtra {
source: source.clone(),
source_sha256: hash.clone(),
invalid: false,
file_match: ctx.file_match.clone(),
parsers,
catalog_description: None,
},
);
} else if let Some(ref source_url) = ctx.source_url {
inject_lintel_extra_from_cache(
value,
ctx.cache,
LintelExtra {
source: source_url.clone(),
source_sha256: String::new(),
invalid: false,
file_match: ctx.file_match.clone(),
parsers,
catalog_description: None,
},
);
}
}
fn inject_lintel_extra(value: &mut serde_json::Value, mut extra: LintelExtra) {
let compile_err = try_compile_schema(value);
if let Some(ref e) = compile_err {
error!(source = %extra.source, error = %e, "schema is invalid after transformation");
}
extra.invalid = compile_err.is_some();
if extra.catalog_description.is_none()
&& let Some(existing) = parse_lintel_extra(value)
{
extra.catalog_description = existing.catalog_description;
}
if let Some(obj) = value.as_object_mut() {
obj.insert(
"x-lintel".to_string(),
serde_json::to_value(extra).expect("LintelExtra is always serializable"),
);
}
}
fn inject_lintel_extra_from_cache(
value: &mut serde_json::Value,
cache: &SchemaCache,
mut extra: LintelExtra,
) {
let Some(hash) = cache.content_hash(&extra.source) else {
return;
};
extra.source_sha256 = hash;
inject_lintel_extra(value, extra);
}
struct NoopRetriever;
impl jsonschema::Retrieve for NoopRetriever {
fn retrieve(
&self,
_uri: &jsonschema::Uri<String>,
) -> Result<serde_json::Value, Box<dyn core::error::Error + Send + Sync>> {
Ok(serde_json::Value::Bool(true))
}
}
fn try_compile_schema(value: &serde_json::Value) -> Option<jsonschema::ValidationError<'static>> {
let err = jsonschema::options()
.with_retriever(NoopRetriever)
.build(value)
.err()?;
let msg = err.to_string();
if msg.contains("does not exist")
|| msg.contains("is not of type")
|| msg.contains("is not valid under any of the schemas listed in the")
|| msg.contains("does not match")
{
return None;
}
Some(err)
}
fn parsers_from_file_match(patterns: &[String]) -> Vec<FileFormat> {
let mut parsers: BTreeSet<FileFormat> = BTreeSet::new();
for pattern in patterns {
let base = pattern.rsplit('/').next().unwrap_or(pattern);
if let Some(dot_pos) = base.rfind('.') {
let format = match &base[dot_pos..] {
".json" => Some(FileFormat::Json),
".jsonc" => Some(FileFormat::Jsonc),
".json5" => Some(FileFormat::Json5),
".yaml" | ".yml" => Some(FileFormat::Yaml),
".toml" => Some(FileFormat::Toml),
".md" | ".mdx" => Some(FileFormat::Markdown),
_ => None,
};
if let Some(f) = format {
parsers.insert(f);
}
}
}
parsers.into_iter().collect()
}