use crate::codec::FormatId;
use crate::document::CadIr;
use cadmpeg_core::dialect::DialectId;
use cadmpeg_core::target::{
DefaultSource, TargetCatalog, TargetDescriptor, TargetRefusal, TargetRefusalKind, TargetToken,
};
use cadmpeg_core::CodecError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetRequest<'a> {
Inherit,
Explicit(&'a str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SourceIdentity {
Other(DefaultSource),
Unrecorded,
Recorded(DialectId),
}
impl SourceIdentity {
const fn recorded(&self) -> Option<&DialectId> {
match self {
Self::Recorded(dialect) => Some(dialect),
Self::Other(_) | Self::Unrecorded => None,
}
}
const fn is_same_format(&self) -> bool {
matches!(self, Self::Unrecorded | Self::Recorded(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolvedTarget<'a> {
Explicit {
index: usize,
entry: &'static TargetDescriptor,
requested: &'a str,
source: SourceIdentity,
},
Inherited {
index: usize,
entry: &'static TargetDescriptor,
source: DialectId,
},
Default {
index: usize,
entry: &'static TargetDescriptor,
source: DefaultSource,
},
Preserved {
source: DialectId,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWrite<'a> {
target: ResolvedTarget<'a>,
available: TargetCatalog,
}
impl<'a> ResolvedWrite<'a> {
#[must_use]
pub const fn target(&self) -> &ResolvedTarget<'a> {
&self.target
}
#[must_use]
pub const fn entry(&self) -> Option<&'static TargetDescriptor> {
match &self.target {
ResolvedTarget::Explicit { entry, .. }
| ResolvedTarget::Inherited { entry, .. }
| ResolvedTarget::Default { entry, .. } => Some(entry),
ResolvedTarget::Preserved { .. } => None,
}
}
#[must_use]
pub const fn index(&self) -> Option<usize> {
match &self.target {
ResolvedTarget::Explicit { index, .. }
| ResolvedTarget::Inherited { index, .. }
| ResolvedTarget::Default { index, .. } => Some(*index),
ResolvedTarget::Preserved { .. } => None,
}
}
#[must_use]
pub const fn target_id(&self) -> &DialectId {
match &self.target {
ResolvedTarget::Explicit { entry, .. }
| ResolvedTarget::Inherited { entry, .. }
| ResolvedTarget::Default { entry, .. } => &entry.id,
ResolvedTarget::Preserved { source } => source,
}
}
#[must_use]
pub fn preserves_source(&self) -> bool {
match &self.target {
ResolvedTarget::Explicit { entry, source, .. } => source.recorded() == Some(&entry.id),
ResolvedTarget::Inherited { .. } | ResolvedTarget::Preserved { .. } => true,
ResolvedTarget::Default { .. } => false,
}
}
#[must_use]
pub fn displaced_source(&self) -> Option<&DialectId> {
match &self.target {
ResolvedTarget::Explicit { entry, source, .. } => {
source.recorded().filter(|source| *source != &entry.id)
}
ResolvedTarget::Inherited { .. }
| ResolvedTarget::Default { .. }
| ResolvedTarget::Preserved { .. } => None,
}
}
#[must_use]
pub fn displacement_message(&self) -> Option<String> {
self.displaced_source().map(|displaced| {
format!(
"source dialect {displaced} was displaced by target dialect {}; the source \
dialect identity is not preserved",
self.target_id()
)
})
}
#[must_use]
pub const fn has_same_format_source(&self) -> bool {
match &self.target {
ResolvedTarget::Explicit { source, .. } => source.is_same_format(),
ResolvedTarget::Inherited { .. } | ResolvedTarget::Preserved { .. } => true,
ResolvedTarget::Default { .. } => false,
}
}
#[must_use]
pub fn source_preservation_eligible(&self) -> bool {
self.has_same_format_source() && self.displaced_source().is_none()
}
#[must_use]
pub fn unavailable(&self, reason: impl Into<String>) -> CodecError {
let reason = reason.into();
let kind = match &self.target {
ResolvedTarget::Explicit {
entry, requested, ..
} => TargetRefusalKind::ExplicitUnavailable {
target: entry.id.clone(),
requested: TargetToken::new(*requested),
reason,
},
ResolvedTarget::Inherited { source, .. } | ResolvedTarget::Preserved { source } => {
TargetRefusalKind::InheritedUnavailable {
source: source.clone(),
reason,
}
}
ResolvedTarget::Default { entry, source, .. } => {
TargetRefusalKind::DefaultUnavailable {
target: entry.id.clone(),
source: source.clone(),
reason,
}
}
};
CodecError::UnsupportedTarget(Box::new(TargetRefusal::new(kind, self.available)))
}
}
mod domain_sealed {
pub trait Sealed {}
impl Sealed for super::DialectFree {}
impl Sealed for super::Catalog {}
}
mod resolution_sealed {
pub trait Sealed {}
impl Sealed for () {}
impl Sealed for super::ResolvedWrite<'_> {}
}
pub trait TargetResolution: resolution_sealed::Sealed {
fn export_target(&self) -> Option<&DialectId>;
}
impl TargetResolution for () {
fn export_target(&self) -> Option<&DialectId> {
None
}
}
impl TargetResolution for ResolvedWrite<'_> {
fn export_target(&self) -> Option<&DialectId> {
Some(self.target_id())
}
}
pub trait TargetDomain: domain_sealed::Sealed {
type Resolved<'a>: TargetResolution;
fn targets(&self, format: FormatId) -> TargetCatalog;
#[doc(hidden)]
fn resolve<'a>(
&self,
ir: &'a CadIr,
request: TargetRequest<'a>,
format: FormatId,
) -> Result<Self::Resolved<'a>, CodecError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DialectFree;
impl TargetDomain for DialectFree {
type Resolved<'a> = ();
fn targets(&self, format: FormatId) -> TargetCatalog {
TargetCatalog::empty(format.as_str())
}
fn resolve<'a>(
&self,
_ir: &'a CadIr,
request: TargetRequest<'a>,
format: FormatId,
) -> Result<(), CodecError> {
match request {
TargetRequest::Inherit => Ok(()),
TargetRequest::Explicit(id) => Err(TargetRefusal::unknown_explicit(
id,
TargetCatalog::empty(format.as_str()),
)
.into()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Catalog(TargetCatalog);
impl Catalog {
#[must_use]
pub const fn new(
targets: &'static [cadmpeg_core::target::TargetDescriptor],
default: Option<usize>,
) -> Self {
Self(TargetCatalog::new(targets, default))
}
}
impl TargetDomain for Catalog {
type Resolved<'a> = ResolvedWrite<'a>;
fn targets(&self, _format: FormatId) -> TargetCatalog {
self.0
}
fn resolve<'a>(
&self,
ir: &'a CadIr,
request: TargetRequest<'a>,
_format: FormatId,
) -> Result<ResolvedWrite<'a>, CodecError> {
resolve_write_request(ir, request, self.0)
}
}
fn source_identity(ir: &CadIr, format: &str) -> SourceIdentity {
let Some(source) = ir.source.as_ref() else {
return SourceIdentity::Other(DefaultSource::NoSource);
};
if source.format() != format {
return SourceIdentity::Other(DefaultSource::ForeignFormat(source.format().to_owned()));
}
match source.dialect() {
Some(matched) => SourceIdentity::Recorded(matched.dialect().clone()),
None => SourceIdentity::Unrecorded,
}
}
fn resolve_write_request<'a>(
ir: &CadIr,
request: TargetRequest<'a>,
catalog: TargetCatalog,
) -> Result<ResolvedWrite<'a>, CodecError> {
let format = catalog.namespace();
let refuse = |kind| CodecError::UnsupportedTarget(Box::new(TargetRefusal::new(kind, catalog)));
let source = source_identity(ir, format);
let target = match request {
TargetRequest::Explicit(requested) => {
let (index, entry) = catalog.find(requested).ok_or_else(|| {
refuse(TargetRefusalKind::UnknownExplicit {
requested: TargetToken::new(requested),
})
})?;
ResolvedTarget::Explicit {
index,
entry,
requested,
source,
}
}
TargetRequest::Inherit => match source {
SourceIdentity::Other(source) => {
let (index, entry) = catalog.default().ok_or_else(|| {
refuse(TargetRefusalKind::NoDefault {
source: source.clone(),
})
})?;
ResolvedTarget::Default {
index,
entry,
source,
}
}
SourceIdentity::Unrecorded => return Err(refuse(TargetRefusalKind::UnrecordedSource)),
SourceIdentity::Recorded(source) => match catalog.find(source.as_str()) {
Some((index, entry)) => ResolvedTarget::Inherited {
index,
entry,
source,
},
None => ResolvedTarget::Preserved { source },
},
},
};
Ok(ResolvedWrite {
target,
available: catalog,
})
}
#[cfg(test)]
mod tests;