1use std::{error::Error, fmt};
4
5use boxferry_model::Application;
6
7use crate::{ConversionOutcome, ConversionPlan, ConversionResult, Diagnostic, LossPolicy, PlanError, TargetProfile};
8
9#[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 #[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 #[must_use]
34 pub const fn success(application: Application) -> Self {
35 Self::new(Some(application), Vec::new(), Vec::new())
36 }
37
38 #[must_use]
40 pub const fn application(&self) -> Option<&Application> {
41 self.application.as_ref()
42 }
43
44 #[must_use]
46 pub fn outcomes(&self) -> &[ConversionOutcome] {
47 &self.outcomes
48 }
49
50 #[must_use]
52 pub fn diagnostics(&self) -> &[Diagnostic] {
53 &self.diagnostics
54 }
55
56 #[must_use]
58 pub fn into_parts(self) -> (Option<Application>, Vec<ConversionOutcome>, Vec<Diagnostic>) {
59 (self.application, self.outcomes, self.diagnostics)
60 }
61}
62
63pub trait ImportAdapter {
65 type Source: ?Sized;
67
68 fn import(&self, source: &Self::Source) -> ImportResult;
70}
71
72pub trait ExportAdapter {
74 type Output;
76
77 fn plan(
83 &self,
84 application: &Application,
85 target: &TargetProfile,
86 ) -> Result<ConversionPlan<Self::Output>, PlanError>;
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91#[non_exhaustive]
92pub enum ConversionError {
93 Import(Vec<Diagnostic>),
95 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
116pub 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#[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 #[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 #[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}