use super::Executable;
use dora_core::{
descriptor::{
Descriptor, DescriptorExt,
validate::{check_type_annotations_full, check_wiring},
},
manifest::{NodeManifest, inject::inject_adjacent_manifests},
types::TypeRegistry,
};
use eyre::{Context, bail};
use std::path::{Path, PathBuf};
#[derive(Debug, clap::Args)]
pub struct Validate {
#[clap(
value_name = "PATH",
value_hint = clap::ValueHint::FilePath,
required_unless_present = "node_manifest",
conflicts_with = "node_manifest"
)]
dataflow: Option<PathBuf>,
#[clap(long, action, conflicts_with = "node_manifest")]
strict_types: bool,
#[clap(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)]
node_manifest: Option<PathBuf>,
#[clap(long, action, conflicts_with = "node_manifest")]
offline: bool,
}
impl Executable for Validate {
fn execute(self) -> eyre::Result<()> {
if let Some(manifest_path) = &self.node_manifest {
return validate_node_manifest(manifest_path);
}
let dataflow = self
.dataflow
.expect("clap guarantees dataflow when --node-manifest is absent");
validate_dataflow(&dataflow, self.strict_types, self.offline)
}
}
fn validate_node_manifest(path: &Path) -> eyre::Result<()> {
println!("Validating node manifest {}...", path.display());
let manifest = NodeManifest::read(path)?;
let registry = TypeRegistry::new();
manifest.validate_strict(®istry)?;
println!("Node manifest OK.");
Ok(())
}
fn validate_dataflow(dataflow: &Path, strict_types: bool, offline: bool) -> eyre::Result<()> {
let working_dir = dataflow
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| std::path::Path::new("."));
println!("Validating {}...", dataflow.display());
let mut descriptor = Descriptor::blocking_read(dataflow)
.with_context(|| {
format!(
"failed to read dataflow at `{}`\n\n \
hint: check the file exists and is valid YAML",
dataflow.display()
)
})?
.expand(working_dir)
.context("failed to expand modules in dataflow descriptor")?;
let strict = strict_types || descriptor.strict_types.unwrap_or(false);
let mut registry = TypeRegistry::new();
let types_dir = working_dir.join("types");
if types_dir.is_dir() {
match registry.load_from_dir(&types_dir) {
Ok(count) if count > 0 => {
println!("Loaded {count} user-defined type(s) from types/");
}
Err(e) => {
bail!("failed to load user types: {e}");
}
_ => {}
}
}
let lockfile_path = super::build::lockfile::BuildLockfile::path_for_dataflow(dataflow, None);
let lockfile = if lockfile_path.exists() {
match super::build::lockfile::BuildLockfile::read_from(&lockfile_path) {
Ok(lf) => {
println!(
" Using hub pins from lockfile `{}`",
lockfile_path.display()
);
Some(lf)
}
Err(e) => {
eprintln!(
" warning: could not read lockfile `{}`: {e} — resolving hub: nodes live",
lockfile_path.display()
);
None
}
}
} else {
None
};
let hub_pins = lockfile.as_ref().map(|l| l.git_sources.clone());
let hub_binary_pins = lockfile.as_ref().map(|l| l.binary_sources.clone());
let hub_resolution = super::build::hub::resolve_hub_nodes(
&mut descriptor,
&mut registry,
offline,
hub_pins.as_ref(),
hub_binary_pins.as_ref(),
false,
&std::collections::BTreeMap::<String, std::path::PathBuf>::new(),
)?;
for note in &hub_resolution.notes {
println!(" {note}");
}
check_wiring(&descriptor).context("wiring check failed")?;
println!("Input/output wiring OK.");
let injection = inject_adjacent_manifests(&mut descriptor, working_dir, &mut registry);
for note in &injection.notes {
println!(" {note}");
}
let result = check_type_annotations_full(&descriptor, ®istry, strict);
for inf in &result.inferences {
println!(" {inf}");
}
let count = hub_resolution.warnings.len() + injection.warnings.len() + result.warnings.len();
if count == 0 {
println!("All type annotations OK.");
} else {
println!("Type warnings:");
for w in &hub_resolution.warnings {
println!(" - {w}");
}
for w in &injection.warnings {
println!(" - {w}");
}
for w in &result.warnings {
println!(" - {w}");
}
println!("{count} type warning(s) found.");
if strict {
bail!("{count} type warning(s) found (--strict mode)");
}
}
Ok(())
}