use crate::snippets::runner::missing_build_dependency;
use crate::snippets::session::SessionSpec;
use crate::snippets::types::{Language, Snippet, ValidationLevel};
use std::collections::{BTreeMap, HashMap};
pub(super) fn enforce_build_dependency(
crate_name: &str,
strict: bool,
snippets: &[Snippet],
sessions: &HashMap<String, SessionSpec>,
requested_level: ValidationLevel,
) -> anyhow::Result<()> {
let missing = missing_build_dependency(snippets, sessions, requested_level);
if missing.is_empty() {
return Ok(());
}
let total: usize = missing.values().sum();
let breakdown = format_breakdown(&missing);
tracing::warn!(
missing_build_dependency = total,
requested_level = %requested_level,
"[{crate_name}] docs.snippets.validation_level = \"{requested_level}\" requires a compiled artifact, but \
{total} snippet(s) have no configured session guaranteed to have produced one before this run reads it \
({breakdown}) -- add a `before` build step under `docs.snippets.sessions.<target>` for the affected \
language(s), or run `alef build` first. This is an ordering gap, not a snippet defect."
);
if strict {
anyhow::bail!(
"strict snippet validation failed for crate `{crate_name}`: {total} snippet(s) validate at \
\"{requested_level}\" with no pipeline-guaranteed build for their language ({breakdown}) -- this is \
an ordering gap: configure `docs.snippets.sessions.<target>.before` to build the artifact before \
validation, or run `alef build` first"
);
}
Ok(())
}
fn format_breakdown(missing: &BTreeMap<Language, usize>) -> String {
missing
.iter()
.map(|(language, count)| format!("{language} {count}"))
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::{SnippetMetadata, SourceOrigin};
use std::path::PathBuf;
fn snippet(language: Language) -> Snippet {
Snippet {
id: None,
path: PathBuf::from("example.md"),
language,
title: None,
code: String::new(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: PathBuf::from("example.md"),
line: 1,
block_index: 0,
},
}
}
#[test]
fn strict_bails_when_a_language_has_no_build_guarantee() {
let sessions = HashMap::new();
let snippets = vec![snippet(Language::Kotlin)];
let error = enforce_build_dependency("fixture", true, &snippets, &sessions, ValidationLevel::Compile)
.expect_err("no session for kotlin must fail strict");
let message = error.to_string();
assert!(message.contains("ordering gap"), "{message}");
assert!(message.contains("kotlin 1"), "{message}");
}
#[test]
fn non_strict_does_not_bail_on_the_same_gap() {
let sessions = HashMap::new();
let snippets = vec![snippet(Language::Kotlin)];
let result = enforce_build_dependency("fixture", false, &snippets, &sessions, ValidationLevel::Compile);
assert!(result.is_ok(), "non-strict must warn, not bail: {result:?}");
}
#[test]
fn strict_does_not_bail_when_every_language_has_a_build_guarantee() {
let sessions = HashMap::from([(
"kotlin".to_string(),
SessionSpec {
language: Language::Kotlin,
working_directory: PathBuf::from("/crate"),
manifest: None,
before: vec!["./gradlew build".to_string()],
env: Default::default(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: Default::default(),
},
)]);
let snippets = vec![snippet(Language::Kotlin)];
let result = enforce_build_dependency("fixture", true, &snippets, &sessions, ValidationLevel::Compile);
assert!(
result.is_ok(),
"a configured before hook must satisfy the gate: {result:?}"
);
}
#[test]
fn strict_does_not_bail_at_syntax_level() {
let sessions = HashMap::new();
let snippets = vec![snippet(Language::Kotlin)];
let result = enforce_build_dependency("fixture", true, &snippets, &sessions, ValidationLevel::Syntax);
assert!(result.is_ok(), "syntax-level validation needs no artifact: {result:?}");
}
}