Skip to main content

boxferry_engine/
adapter.rs

1//! Native adapter contracts and shared public orchestration.
2
3use std::{error::Error, fmt};
4
5use boxferry_model::Application;
6
7use crate::{ConversionOutcome, ConversionPlan, ConversionResult, Diagnostic, LossPolicy, PlanError, TargetProfile};
8
9/// Recoverable result of importing a native source model.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct ImportResult {
12    application: Option<Application>,
13    outcomes: Vec<ConversionOutcome>,
14    diagnostics: Vec<Diagnostic>,
15}
16
17impl ImportResult {
18    /// Creates an import result with an optional neutral application and source-mapping decisions.
19    #[must_use]
20    pub const fn new(
21        application: Option<Application>,
22        outcomes: Vec<ConversionOutcome>,
23        diagnostics: Vec<Diagnostic>,
24    ) -> Self {
25        Self {
26            application,
27            outcomes,
28            diagnostics,
29        }
30    }
31
32    /// Creates a successful import without diagnostics.
33    #[must_use]
34    pub const fn success(application: Application) -> Self {
35        Self::new(Some(application), Vec::new(), Vec::new())
36    }
37
38    /// Returns the recoverable application, when available.
39    #[must_use]
40    pub const fn application(&self) -> Option<&Application> {
41        self.application.as_ref()
42    }
43
44    /// Returns source-to-neutral-model fidelity decisions.
45    #[must_use]
46    pub fn outcomes(&self) -> &[ConversionOutcome] {
47        &self.outcomes
48    }
49
50    /// Returns import diagnostics.
51    #[must_use]
52    pub fn diagnostics(&self) -> &[Diagnostic] {
53        &self.diagnostics
54    }
55
56    /// Decomposes the import result.
57    #[must_use]
58    pub fn into_parts(self) -> (Option<Application>, Vec<ConversionOutcome>, Vec<Diagnostic>) {
59        (self.application, self.outcomes, self.diagnostics)
60    }
61}
62
63/// Maps one native source model into the format-independent application model.
64pub trait ImportAdapter {
65    /// Native source model accepted by this adapter.
66    type Source: ?Sized;
67
68    /// Imports one source without reading ambient process state.
69    fn import(&self, source: &Self::Source) -> ImportResult;
70}
71
72/// Plans one neutral application for a native target model.
73pub trait ExportAdapter {
74    /// Native target candidate returned by this adapter.
75    type Output;
76
77    /// Builds a validated candidate plan for the explicit target profile.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`PlanError`] when the adapter violates plan invariants.
82    fn plan(
83        &self,
84        application: &Application,
85        target: &TargetProfile,
86    ) -> Result<ConversionPlan<Self::Output>, PlanError>;
87}
88
89/// Failure before a policy-authorized conversion result can be created.
90#[derive(Clone, Debug, Eq, PartialEq)]
91#[non_exhaustive]
92pub enum ConversionError {
93    /// Import returned no neutral application or an error diagnostic.
94    Import(Vec<Diagnostic>),
95    /// A target adapter returned a structurally invalid plan.
96    InvalidPlan(PlanError),
97}
98
99impl fmt::Display for ConversionError {
100    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::Import(diagnostics) => {
103                write!(
104                    formatter,
105                    "source import failed with {} diagnostic(s)",
106                    diagnostics.len()
107                )
108            }
109            Self::InvalidPlan(error) => write!(formatter, "target adapter returned an invalid plan: {error}"),
110        }
111    }
112}
113
114impl Error for ConversionError {}
115
116/// Runs the same explicit import-plan-authorize path used by the `BoxFerry` CLI.
117///
118/// # Errors
119///
120/// Returns [`ConversionError::Import`] when import has no usable application or
121/// contains an error diagnostic, and [`ConversionError::InvalidPlan`] when the
122/// export adapter violates plan invariants.
123pub fn convert<I, E>(
124    importer: &I,
125    source: &I::Source,
126    exporter: &E,
127    target: &TargetProfile,
128    policy: LossPolicy,
129) -> Result<ConversionResult<E::Output>, ConversionError>
130where
131    I: ImportAdapter,
132    E: ExportAdapter,
133{
134    let import = importer.import(source);
135    let (application, import_outcomes, import_diagnostics) = import.into_parts();
136    if application.is_none()
137        || import_diagnostics
138            .iter()
139            .any(|diagnostic| diagnostic.severity() == crate::Severity::Error)
140    {
141        return Err(ConversionError::Import(import_diagnostics));
142    }
143    let Some(application) = application else {
144        return Err(ConversionError::Import(import_diagnostics));
145    };
146    let mut plan = exporter
147        .plan(&application, target)
148        .map_err(ConversionError::InvalidPlan)?;
149    plan.extend_import(import_outcomes, import_diagnostics)
150        .map_err(ConversionError::InvalidPlan)?;
151    Ok(plan.authorize(policy))
152}
153
154/// Deterministic adapter for public API tests and embedding examples.
155#[derive(Clone, Debug, Eq, PartialEq)]
156pub struct InMemoryAdapter<T> {
157    output: T,
158    outcomes: Vec<ConversionOutcome>,
159    diagnostics: Vec<Diagnostic>,
160}
161
162impl<T> InMemoryAdapter<T> {
163    /// Creates an adapter that reports one exact application outcome.
164    #[must_use]
165    pub fn exact(output: T) -> Self {
166        Self {
167            output,
168            outcomes: vec![ConversionOutcome::exact("application")],
169            diagnostics: Vec::new(),
170        }
171    }
172
173    /// Creates an adapter with caller-selected validated-plan inputs.
174    #[must_use]
175    pub const fn new(output: T, outcomes: Vec<ConversionOutcome>, diagnostics: Vec<Diagnostic>) -> Self {
176        Self {
177            output,
178            outcomes,
179            diagnostics,
180        }
181    }
182}
183
184impl<T> ImportAdapter for InMemoryAdapter<T> {
185    type Source = Application;
186
187    fn import(&self, source: &Self::Source) -> ImportResult {
188        ImportResult::success(source.clone())
189    }
190}
191
192impl<T: Clone> ExportAdapter for InMemoryAdapter<T> {
193    type Output = T;
194
195    fn plan(
196        &self,
197        _application: &Application,
198        _target: &TargetProfile,
199    ) -> Result<ConversionPlan<Self::Output>, PlanError> {
200        ConversionPlan::new(
201            Some(self.output.clone()),
202            self.outcomes.clone(),
203            self.diagnostics.clone(),
204        )
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use boxferry_model::{Application, Identifier};
211
212    use super::{InMemoryAdapter, convert};
213    use crate::{LossPolicy, PlatformVersion, TargetProfile};
214
215    #[test]
216    fn in_memory_adapter_proves_the_public_orchestration_path() -> Result<(), String> {
217        let application = Application::new(Identifier::new("example").map_err(|error| error.to_string())?);
218        let adapter = InMemoryAdapter::exact("rendered target".to_owned());
219        let target = TargetProfile::new("test-target", PlatformVersion::new(1, 0, 0), None)
220            .map_err(|error| error.to_string())?;
221
222        let result = convert(&adapter, &application, &adapter, &target, LossPolicy::ExactOnly)
223            .map_err(|error| error.to_string())?;
224        assert_eq!(result.output().map(String::as_str), Some("rendered target"));
225        assert!(!result.is_blocked());
226        Ok(())
227    }
228}