use std::fs;
use std::path::Path;
use std::str::FromStr;
use crate::capabilities::{evaluate, CapabilityProfile, CapabilityReport, CapabilityResult};
use crate::diagnostics::ValidationReport;
use crate::model::PipelineContract;
use crate::plan::{self, PipelinePlan, PlanResult};
use super::adapters::{adapter_for, validate_relative_path};
use super::artifact::{BindingBundle, BindingTarget};
use super::diagnostics::{self, write_failed};
#[derive(Debug, Clone)]
pub struct BindContext<'a> {
pub profile_identity: &'a str,
pub capability: &'a CapabilityReport,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BindingResult {
Ok(Box<BindingBundle>),
Err {
diagnostics: ValidationReport,
capability: Option<Box<CapabilityReport>>,
},
}
impl BindingResult {
pub fn bundle(self) -> Option<BindingBundle> {
match self {
Self::Ok(bundle) => Some(*bundle),
Self::Err { .. } => None,
}
}
pub fn as_bundle(&self) -> Option<&BindingBundle> {
match self {
Self::Ok(bundle) => Some(bundle),
Self::Err { .. } => None,
}
}
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok(_))
}
pub fn report(&self) -> Option<&ValidationReport> {
match self {
Self::Ok(_) => None,
Self::Err { diagnostics, .. } => Some(diagnostics),
}
}
pub fn capability_report(&self) -> Option<&CapabilityReport> {
match self {
Self::Ok(_) => None,
Self::Err { capability, .. } => capability.as_deref(),
}
}
fn err(diagnostics: ValidationReport) -> Self {
Self::Err {
diagnostics,
capability: None,
}
}
fn err_with_capability(diagnostics: ValidationReport, report: CapabilityReport) -> Self {
Self::Err {
diagnostics,
capability: Some(Box::new(report)),
}
}
}
pub fn parse_target(name: &str) -> Result<BindingTarget, ValidationReport> {
BindingTarget::from_str(name)
.map_err(|_| diagnostics::report_error(diagnostics::unknown_target(name)))
}
pub fn bind(
plan: &PipelinePlan,
profile: &CapabilityProfile,
target: BindingTarget,
) -> BindingResult {
let capability = match evaluate(plan, profile) {
CapabilityResult::Ok(report) => report,
CapabilityResult::Err {
report,
diagnostics,
} => {
return BindingResult::err_with_capability(
diagnostics::report_capability_gate(diagnostics),
*report,
);
}
};
let ctx = BindContext {
profile_identity: &profile.identity,
capability: &capability,
};
let adapter = adapter_for(target);
match adapter.translate(plan, &ctx) {
Ok(files) => {
if files.is_empty() {
return BindingResult::err(diagnostics::report_error(
diagnostics::translation_incomplete("binding adapter produced no artifacts"),
));
}
for file in &files {
if let Err(report) = validate_relative_path(&file.relative_path) {
return BindingResult::err(report);
}
}
BindingResult::Ok(Box::new(BindingBundle {
target,
contract_id: plan.contract_id.clone(),
contract_version: plan.contract_version.clone(),
profile_identity: profile.identity.clone(),
files,
capability: *capability,
}))
}
Err(report) => BindingResult::err(report),
}
}
pub fn bind_contract(
contract: &PipelineContract,
profile: &CapabilityProfile,
target: BindingTarget,
) -> BindingResult {
match plan::plan(contract) {
PlanResult::Ok(planned) => bind(&planned, profile, target),
PlanResult::Err(report) => BindingResult::err(report),
}
}
pub fn write_bundle(bundle: &BindingBundle, out_dir: &Path) -> Result<(), ValidationReport> {
if let Err(err) = fs::create_dir_all(out_dir) {
return Err(diagnostics::report_error(write_failed(format!(
"failed to create directory {}: {err}",
out_dir.display()
))));
}
for file in &bundle.files {
validate_relative_path(&file.relative_path)?;
let path = out_dir.join(Path::new(&file.relative_path));
if let Some(parent) = path.parent() {
if let Err(err) = fs::create_dir_all(parent) {
return Err(diagnostics::report_error(write_failed(format!(
"failed to create directory {}: {err}",
parent.display()
))));
}
}
if let Err(err) = fs::write(&path, &file.content) {
return Err(diagnostics::report_error(write_failed(format!(
"failed to write {}: {err}",
path.display()
))));
}
}
Ok(())
}