use std::{error::Error, fmt};
use boxferry_model::Application;
use crate::{ConversionOutcome, ConversionPlan, ConversionResult, Diagnostic, LossPolicy, PlanError, TargetProfile};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImportResult {
application: Option<Application>,
outcomes: Vec<ConversionOutcome>,
diagnostics: Vec<Diagnostic>,
}
impl ImportResult {
#[must_use]
pub const fn new(
application: Option<Application>,
outcomes: Vec<ConversionOutcome>,
diagnostics: Vec<Diagnostic>,
) -> Self {
Self {
application,
outcomes,
diagnostics,
}
}
#[must_use]
pub const fn success(application: Application) -> Self {
Self::new(Some(application), Vec::new(), Vec::new())
}
#[must_use]
pub const fn application(&self) -> Option<&Application> {
self.application.as_ref()
}
#[must_use]
pub fn outcomes(&self) -> &[ConversionOutcome] {
&self.outcomes
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn into_parts(self) -> (Option<Application>, Vec<ConversionOutcome>, Vec<Diagnostic>) {
(self.application, self.outcomes, self.diagnostics)
}
}
pub trait ImportAdapter {
type Source: ?Sized;
fn import(&self, source: &Self::Source) -> ImportResult;
}
pub trait ExportAdapter {
type Output;
fn plan(
&self,
application: &Application,
target: &TargetProfile,
) -> Result<ConversionPlan<Self::Output>, PlanError>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ConversionError {
Import(Vec<Diagnostic>),
InvalidPlan(PlanError),
}
impl fmt::Display for ConversionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Import(diagnostics) => {
write!(
formatter,
"source import failed with {} diagnostic(s)",
diagnostics.len()
)
}
Self::InvalidPlan(error) => write!(formatter, "target adapter returned an invalid plan: {error}"),
}
}
}
impl Error for ConversionError {}
pub fn convert<I, E>(
importer: &I,
source: &I::Source,
exporter: &E,
target: &TargetProfile,
policy: LossPolicy,
) -> Result<ConversionResult<E::Output>, ConversionError>
where
I: ImportAdapter,
E: ExportAdapter,
{
let import = importer.import(source);
let (application, import_outcomes, import_diagnostics) = import.into_parts();
if application.is_none()
|| import_diagnostics
.iter()
.any(|diagnostic| diagnostic.severity() == crate::Severity::Error)
{
return Err(ConversionError::Import(import_diagnostics));
}
let Some(application) = application else {
return Err(ConversionError::Import(import_diagnostics));
};
let mut plan = exporter
.plan(&application, target)
.map_err(ConversionError::InvalidPlan)?;
plan.extend_import(import_outcomes, import_diagnostics)
.map_err(ConversionError::InvalidPlan)?;
Ok(plan.authorize(policy))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InMemoryAdapter<T> {
output: T,
outcomes: Vec<ConversionOutcome>,
diagnostics: Vec<Diagnostic>,
}
impl<T> InMemoryAdapter<T> {
#[must_use]
pub fn exact(output: T) -> Self {
Self {
output,
outcomes: vec![ConversionOutcome::exact("application")],
diagnostics: Vec::new(),
}
}
#[must_use]
pub const fn new(output: T, outcomes: Vec<ConversionOutcome>, diagnostics: Vec<Diagnostic>) -> Self {
Self {
output,
outcomes,
diagnostics,
}
}
}
impl<T> ImportAdapter for InMemoryAdapter<T> {
type Source = Application;
fn import(&self, source: &Self::Source) -> ImportResult {
ImportResult::success(source.clone())
}
}
impl<T: Clone> ExportAdapter for InMemoryAdapter<T> {
type Output = T;
fn plan(
&self,
_application: &Application,
_target: &TargetProfile,
) -> Result<ConversionPlan<Self::Output>, PlanError> {
ConversionPlan::new(
Some(self.output.clone()),
self.outcomes.clone(),
self.diagnostics.clone(),
)
}
}
#[cfg(test)]
mod tests {
use boxferry_model::{Application, Identifier};
use super::{InMemoryAdapter, convert};
use crate::{LossPolicy, PlatformVersion, TargetProfile};
#[test]
fn in_memory_adapter_proves_the_public_orchestration_path() -> Result<(), String> {
let application = Application::new(Identifier::new("example").map_err(|error| error.to_string())?);
let adapter = InMemoryAdapter::exact("rendered target".to_owned());
let target = TargetProfile::new("test-target", PlatformVersion::new(1, 0, 0), None)
.map_err(|error| error.to_string())?;
let result = convert(&adapter, &application, &adapter, &target, LossPolicy::ExactOnly)
.map_err(|error| error.to_string())?;
assert_eq!(result.output().map(String::as_str), Some("rendered target"));
assert!(!result.is_blocked());
Ok(())
}
}