use super::pipeline_config::load_pipeline_config;
use super::PackageCommand;
use crate::cli::{run_package_convert, run_package_convert_project, run_package_init};
use crate::errors::ErrorLocationValue;
use crate::errors::{classify_validation_code, classify_verification_code, CliError};
use crate::input::{read_fixture_observations, read_package_manifest};
use crate::output::print_success;
use biors_core::package::{
compare_package_manifest_schemas, diff_package_manifests, inspect_package_manifest,
plan_package_schema_migration, plan_runtime_bridge,
validate_package_manifest_artifacts_with_manifest_path_and_pipeline_config_validator,
PackageManifest, PackageValidationReport, ReferencedConfigError,
};
use biors_core::verification::verify_package_outputs_with_observation_base;
use serde::Serialize;
use serde_json::json;
use std::{
fs,
path::{Path, PathBuf},
};
pub(crate) fn run_package_command(command: PackageCommand) -> Result<(), CliError> {
match command {
PackageCommand::Bridge { path } => run_package_bridge(path),
PackageCommand::Compatibility { left, right } => run_package_compatibility(left, right),
PackageCommand::Convert(args) => run_package_convert(*args),
PackageCommand::ConvertProject(args) => run_package_convert_project(*args),
PackageCommand::Diff { left, right } => run_package_diff(left, right),
PackageCommand::Init(args) => run_package_init(*args),
PackageCommand::Inspect { path } => run_package_inspect(path),
PackageCommand::Migrate { path, to } => run_package_migrate(path, to.into()),
PackageCommand::Validate { path } => run_package_validate(path),
PackageCommand::Verify {
manifest,
observations,
} => run_package_verify(manifest, observations),
}
}
fn run_package_bridge(path: PathBuf) -> Result<(), CliError> {
let manifest_path = path.clone();
let (manifest, manifest_base_dir) = read_package_manifest(path)?;
let report = plan_runtime_bridge(&manifest);
let validation =
validate_cli_package_manifest_artifacts(&manifest, &manifest_base_dir, &manifest_path);
if !validation.valid || !report.ready {
let message = join_failure_messages(
validation
.issues
.iter()
.chain(report.blocking_issues.iter())
.map(String::as_str),
);
return Err(CliError::ValidationDetails {
code: "package.bridge_not_ready",
message,
location: Some("manifest".to_string()),
details: json!({
"validation": validation,
"bridge": report,
}),
});
}
print_success(None, report)
}
fn run_package_compatibility(left: PathBuf, right: PathBuf) -> Result<(), CliError> {
let (left_manifest, _) = read_package_manifest(left.clone())?;
let (right_manifest, _) = read_package_manifest(right.clone())?;
let report = compare_package_manifest_schemas(
&left.display().to_string(),
&right.display().to_string(),
&left_manifest,
&right_manifest,
);
print_success(None, report)
}
fn run_package_diff(left: PathBuf, right: PathBuf) -> Result<(), CliError> {
let left_bytes = read_manifest_bytes(&left)?;
let right_bytes = read_manifest_bytes(&right)?;
let (left_manifest, _) = read_package_manifest(left.clone())?;
let (right_manifest, _) = read_package_manifest(right.clone())?;
let report = diff_package_manifests(
&left.display().to_string(),
&right.display().to_string(),
&left_manifest,
&right_manifest,
&left_bytes,
&right_bytes,
);
print_success(None, report)
}
fn run_package_inspect(path: PathBuf) -> Result<(), CliError> {
let (manifest, _) = read_package_manifest(path)?;
let summary = inspect_package_manifest(&manifest);
print_success(None, summary)
}
fn run_package_migrate(
path: PathBuf,
to: biors_core::package::SchemaVersion,
) -> Result<(), CliError> {
let (manifest, _) = read_package_manifest(path)?;
let Some(report) = plan_package_schema_migration(&manifest, to) else {
return Err(CliError::Validation {
code: "package.migration_unsupported",
message: format!(
"no package manifest migration plan from '{}' to '{}'",
manifest.schema_version, to
),
location: Some("manifest".to_string()),
});
};
print_success(None, report)
}
fn run_package_validate(path: PathBuf) -> Result<(), CliError> {
let manifest_path = path.clone();
let (manifest, manifest_base_dir) = read_package_manifest(path)?;
let report =
validate_cli_package_manifest_artifacts(&manifest, &manifest_base_dir, &manifest_path);
if !report.valid {
let message = join_failure_messages(report.issues.iter().map(String::as_str));
return Err(CliError::ValidationDetails {
code: classify_validation_code(&report),
message,
location: Some("manifest".to_string()),
details: report_details(&report),
});
}
print_success(None, report)
}
fn run_package_verify(manifest: PathBuf, observations: PathBuf) -> Result<(), CliError> {
let manifest_path = manifest.clone();
let (manifest, manifest_base_dir) = read_package_manifest(manifest)?;
let (observations, observations_base_dir) = read_fixture_observations(observations)?;
let validation =
validate_cli_package_manifest_artifacts(&manifest, &manifest_base_dir, &manifest_path);
if !validation.valid {
let message = join_failure_messages(validation.issues.iter().map(String::as_str));
return Err(CliError::ValidationDetails {
code: classify_validation_code(&validation),
message,
location: Some("manifest".to_string()),
details: json!({
"validation": validation,
}),
});
}
let report = verify_package_outputs_with_observation_base(
&manifest,
&observations,
&manifest_base_dir,
&observations_base_dir,
);
if report.failed > 0 {
let message = join_failure_messages(
report
.results
.iter()
.filter_map(|result| result.issue.as_deref()),
);
return Err(CliError::ValidationDetails {
code: classify_verification_code(&report),
message,
location: Some("fixtures".to_string()),
details: report_details(&report),
});
}
print_success(None, report)
}
pub(crate) fn validate_cli_package_manifest_artifacts(
manifest: &PackageManifest,
manifest_base_dir: &Path,
manifest_path: &Path,
) -> PackageValidationReport {
let manifest_path = (manifest_path.as_os_str() != "-").then_some(manifest_path);
validate_package_manifest_artifacts_with_manifest_path_and_pipeline_config_validator(
manifest,
manifest_base_dir,
manifest_path,
Some(&|path| {
load_pipeline_config(path)
.and_then(|resolved| {
validate_package_pipeline_input(manifest_base_dir, path, &resolved)
})
.map_err(|error| {
ReferencedConfigError::new(
error.code(),
error.to_string(),
error.location().map(location_label),
)
})
}),
)
}
fn validate_package_pipeline_input(
manifest_base_dir: &Path,
config_path: &Path,
resolved: &super::pipeline_config::ResolvedPipelineConfig,
) -> Result<(), CliError> {
let declared_input = Path::new(&resolved.config.input.path);
if declared_input.is_absolute() {
return Err(CliError::Validation {
code: "pipeline.invalid_config",
message: "package pipeline input.path must be package-relative".to_string(),
location: Some("input.path".to_string()),
});
}
let package_root = canonicalize_package_validation_path(manifest_base_dir)?;
let input_path = config_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(declared_input);
let input_canonical = canonicalize_package_validation_path(&input_path)?;
if !input_canonical.starts_with(&package_root) {
return Err(CliError::Validation {
code: "pipeline.invalid_config",
message: "package pipeline input.path must stay inside the package root".to_string(),
location: Some("input.path".to_string()),
});
}
Ok(())
}
fn canonicalize_package_validation_path(path: &Path) -> Result<PathBuf, CliError> {
path.canonicalize().map_err(|source| CliError::Read {
path: path.to_path_buf(),
source,
})
}
fn location_label(location: ErrorLocationValue) -> String {
match location {
ErrorLocationValue::Label(label) => label,
ErrorLocationValue::Core(location) => format!("{location:?}"),
}
}
fn read_manifest_bytes(path: &PathBuf) -> Result<Vec<u8>, CliError> {
fs::read(path).map_err(|source| CliError::Read {
path: path.clone(),
source,
})
}
fn join_failure_messages<'a>(messages: impl Iterator<Item = &'a str>) -> String {
let joined = messages
.filter(|message| !message.is_empty())
.collect::<Vec<_>>()
.join("; ");
if joined.is_empty() {
"package command failed".to_string()
} else {
joined
}
}
fn report_details<T: Serialize>(report: &T) -> serde_json::Value {
serde_json::to_value(report).unwrap_or_else(|error| {
json!({
"serialization_error": error.to_string(),
})
})
}