dpcs/binding/
framework.rs1use std::fs;
4use std::path::Path;
5use std::str::FromStr;
6
7use crate::capabilities::{evaluate, CapabilityProfile, CapabilityReport, CapabilityResult};
8use crate::diagnostics::ValidationReport;
9use crate::model::PipelineContract;
10use crate::paths::join_under_root;
11use crate::plan::{self, PipelinePlan, PlanResult};
12
13use super::adapters::{adapter_for, validate_relative_path};
14use super::artifact::{BindingBundle, BindingTarget};
15use super::diagnostics::{self, write_failed};
16
17#[derive(Debug, Clone)]
19pub struct BindContext<'a> {
20 pub profile_identity: &'a str,
22 pub capability: &'a CapabilityReport,
24}
25
26#[derive(Debug, Clone, PartialEq)]
28pub enum BindingResult {
29 Ok(Box<BindingBundle>),
31 Err {
33 diagnostics: ValidationReport,
35 capability: Option<Box<CapabilityReport>>,
37 },
38}
39
40impl BindingResult {
41 pub fn bundle(self) -> Option<BindingBundle> {
43 match self {
44 Self::Ok(bundle) => Some(*bundle),
45 Self::Err { .. } => None,
46 }
47 }
48
49 pub fn as_bundle(&self) -> Option<&BindingBundle> {
51 match self {
52 Self::Ok(bundle) => Some(bundle),
53 Self::Err { .. } => None,
54 }
55 }
56
57 pub fn is_ok(&self) -> bool {
59 matches!(self, Self::Ok(_))
60 }
61
62 pub fn report(&self) -> Option<&ValidationReport> {
64 match self {
65 Self::Ok(_) => None,
66 Self::Err { diagnostics, .. } => Some(diagnostics),
67 }
68 }
69
70 pub fn capability_report(&self) -> Option<&CapabilityReport> {
72 match self {
73 Self::Ok(_) => None,
74 Self::Err { capability, .. } => capability.as_deref(),
75 }
76 }
77
78 fn err(diagnostics: ValidationReport) -> Self {
79 Self::Err {
80 diagnostics,
81 capability: None,
82 }
83 }
84
85 fn err_with_capability(diagnostics: ValidationReport, report: CapabilityReport) -> Self {
86 Self::Err {
87 diagnostics,
88 capability: Some(Box::new(report)),
89 }
90 }
91}
92
93pub fn parse_target(name: &str) -> Result<BindingTarget, ValidationReport> {
97 BindingTarget::from_str(name)
98 .map_err(|_| diagnostics::report_error(diagnostics::unknown_target(name)))
99}
100
101pub fn bind(
107 plan: &PipelinePlan,
108 profile: &CapabilityProfile,
109 target: BindingTarget,
110) -> BindingResult {
111 let capability = match evaluate(plan, profile) {
112 CapabilityResult::Ok(report) => report,
113 CapabilityResult::Err {
114 report,
115 diagnostics,
116 } => {
117 return BindingResult::err_with_capability(
118 diagnostics::report_capability_gate(diagnostics),
119 *report,
120 );
121 }
122 };
123
124 let ctx = BindContext {
125 profile_identity: &profile.identity,
126 capability: &capability,
127 };
128
129 let adapter = adapter_for(target);
130 match adapter.translate(plan, &ctx) {
131 Ok(files) => {
132 if files.is_empty() {
133 return BindingResult::err(diagnostics::report_error(
134 diagnostics::translation_incomplete("binding adapter produced no artifacts"),
135 ));
136 }
137 for file in &files {
138 if let Err(report) = validate_relative_path(&file.relative_path) {
139 return BindingResult::err(report);
140 }
141 }
142 BindingResult::Ok(Box::new(BindingBundle {
143 target,
144 contract_id: plan.contract_id.clone(),
145 contract_version: plan.contract_version.clone(),
146 profile_identity: profile.identity.clone(),
147 files,
148 capability: *capability,
149 }))
150 }
151 Err(report) => BindingResult::err(report),
152 }
153}
154
155pub fn bind_contract(
160 contract: &PipelineContract,
161 profile: &CapabilityProfile,
162 target: BindingTarget,
163) -> BindingResult {
164 bind_contract_with_resolve(contract, profile, target, None)
168}
169
170pub fn bind_contract_with_resolve(
174 contract: &PipelineContract,
175 profile: &CapabilityProfile,
176 target: BindingTarget,
177 resolve: Option<&crate::resolve::ResolveOptions>,
178) -> BindingResult {
179 match plan::plan_with_resolve(contract, resolve) {
180 PlanResult::Ok(planned) => bind(&planned, profile, target),
181 PlanResult::Err(report) => BindingResult::err(report),
182 }
183}
184
185pub fn write_bundle(bundle: &BindingBundle, out_dir: &Path) -> Result<(), ValidationReport> {
191 if let Err(err) = fs::create_dir_all(out_dir) {
192 return Err(diagnostics::report_error(write_failed(format!(
193 "failed to create directory {}: {err}",
194 out_dir.display()
195 ))));
196 }
197 for file in &bundle.files {
198 validate_relative_path(&file.relative_path)?;
199 let path = join_under_root(out_dir, &file.relative_path).map_err(|err| {
200 diagnostics::report_error(write_failed(format!(
201 "unsafe binding path {}: {err}",
202 file.relative_path
203 )))
204 })?;
205 if let Some(parent) = path.parent() {
206 if let Err(err) = fs::create_dir_all(parent) {
207 return Err(diagnostics::report_error(write_failed(format!(
208 "failed to create directory {}: {err}",
209 parent.display()
210 ))));
211 }
212 }
213 if let Err(err) = fs::write(&path, &file.content) {
214 return Err(diagnostics::report_error(write_failed(format!(
215 "failed to write {}: {err}",
216 path.display()
217 ))));
218 }
219 }
220 Ok(())
221}