use std::sync::{Arc, OnceLock};
use edifact_rs::directory_validator::SegmentDefinition;
use edifact_rs::{DirectoryValidator, ProfileRulePack};
use crate::{Error, MessageType, Pruefidentifikator, Release, generated};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PidSource {
#[default]
BgmDe1004,
RffZ13,
}
pub trait Profile: Send + Sync {
fn message_type(&self) -> MessageType;
fn release(&self) -> &Release;
fn valid_from(&self) -> Option<time::Date>;
fn valid_until(&self) -> Option<time::Date> {
None
}
fn ahb_revision(&self) -> Option<&'static str> {
None
}
fn source_document(&self) -> Option<&'static str> {
None
}
fn pid_source(&self) -> PidSource {
PidSource::BgmDe1004
}
fn mig_rule_pack(&self) -> Arc<ProfileRulePack>;
fn ahb_rule_pack(&self, pid: Option<Pruefidentifikator>) -> Arc<ProfileRulePack>;
fn is_code_valid(&self, de_id: &str, code: &str) -> bool;
fn suggest_code(&self, de_id: &str, code: &str) -> Option<&'static str>;
fn segment_lookup(&self, tag: &str) -> Option<&'static SegmentDefinition>;
fn code_list(&self, de_id: &str) -> Option<&'static [&'static str]>;
fn directory_validator(&self) -> &'static DirectoryValidator;
fn group_schema(&self) -> &'static [edifact_rs::GroupDef] {
&[]
}
}
const _: () = {
fn _assert_object_safe(_: &dyn Profile) {}
};
pub const TRANSITION_GRACE_DAYS: i64 = 7;
pub enum TransitionState<'r> {
Stable {
profile: &'r dyn Profile,
},
Transition {
outgoing: &'r dyn Profile,
incoming: &'r dyn Profile,
},
None,
}
impl std::fmt::Debug for TransitionState<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransitionState::Stable { profile } => f
.debug_struct("Stable")
.field("release", &profile.release().as_str())
.field("message_type", &profile.message_type())
.finish(),
TransitionState::Transition { outgoing, incoming } => f
.debug_struct("Transition")
.field("outgoing", &outgoing.release().as_str())
.field("incoming", &incoming.release().as_str())
.finish(),
TransitionState::None => write!(f, "None"),
}
}
}
#[derive(Clone)]
pub struct ReleaseRegistry {
profiles: Vec<&'static dyn Profile>,
index: std::collections::HashMap<MessageType, std::collections::HashMap<Box<str>, Vec<usize>>>,
transition_grace_days: i64,
}
static REGISTRY: OnceLock<std::sync::Arc<ReleaseRegistry>> = OnceLock::new();
impl ReleaseRegistry {
#[must_use]
pub fn new(profiles: Vec<&'static dyn Profile>) -> Self {
let mut index: std::collections::HashMap<
MessageType,
std::collections::HashMap<Box<str>, Vec<usize>>,
> = std::collections::HashMap::new();
for (i, p) in profiles.iter().enumerate() {
index
.entry(p.message_type())
.or_default()
.entry(p.release().as_str().into())
.or_default()
.push(i);
}
for type_map in index.values_mut() {
for candidates in type_map.values_mut() {
candidates.sort_by(|&a, &b| {
let va = profiles[a].valid_from();
let vb = profiles[b].valid_from();
va.cmp(&vb)
});
}
}
Self {
profiles,
index,
transition_grace_days: TRANSITION_GRACE_DAYS,
}
}
#[must_use]
pub fn with_transition_grace_days(mut self, days: i64) -> Self {
self.transition_grace_days = days;
self
}
#[must_use]
pub fn transition_grace_days(&self) -> i64 {
self.transition_grace_days
}
#[must_use]
pub fn global() -> &'static Self {
Self::global_arc()
}
#[must_use]
pub fn global_arc() -> &'static std::sync::Arc<ReleaseRegistry> {
REGISTRY.get_or_init(|| {
let mut profiles: Vec<&'static dyn Profile> = Vec::new();
generated::register_profiles(&mut profiles);
std::sync::Arc::new(Self::new(profiles))
})
}
pub fn profile(
&self,
message_type: MessageType,
release: &Release,
) -> Result<&dyn Profile, Error> {
self.profile_on(
message_type,
release,
time::OffsetDateTime::now_utc().date(),
)
}
pub fn profile_on(
&self,
message_type: MessageType,
release: &Release,
date: time::Date,
) -> Result<&dyn Profile, Error> {
let candidates = self
.index
.get(&message_type)
.and_then(|m| m.get(release.as_str()))
.ok_or_else(|| {
let mt_str = message_type.as_str();
if let Some(feat) =
crate::generated::archived_profile_feature(mt_str, release.as_str())
{
Error::ProfileArchived {
message_type,
release: release.clone(),
feature_flag: feat,
}
} else {
Error::ProfileNotFound {
message_type,
release: release.clone(),
}
}
})?;
if let Some(&idx) = candidates
.iter()
.rfind(|&&i| self.profiles[i].valid_from().is_none_or(|vf| vf <= date))
{
#[cfg(feature = "tracing")]
if candidates.len() > 1 {
tracing::debug!(
message_type = ?message_type,
release = release.as_str(),
date = %date,
resolved_to = self.profiles[idx].release().as_str(),
candidates = candidates.len(),
"wire_code_collision_resolved",
);
}
return Ok(self.profiles[idx]);
}
let all_undated = candidates
.iter()
.all(|&i| self.profiles[i].valid_from().is_none());
if all_undated {
let idx = *candidates.last().expect("candidates is non-empty");
return Ok(self.profiles[idx]);
}
let (_earliest_idx, earliest_vf) = candidates
.iter()
.filter_map(|&i| self.profiles[i].valid_from().map(|vf| (i, vf)))
.min_by_key(|(_, vf)| *vf)
.expect("at least one dated candidate exists");
Err(Error::ProfileNotYetActive {
message_type,
release: release.clone(),
valid_from: earliest_vf,
date,
})
}
#[must_use]
pub fn all_profiles(&self) -> &[&'static dyn Profile] {
&self.profiles
}
pub fn profiles_for(&self, message_type: MessageType) -> impl Iterator<Item = &dyn Profile> {
self.profiles
.iter()
.filter(move |p| p.message_type() == message_type)
.copied()
}
#[must_use]
pub fn pid_has_ahb_rules(&self, message_type: MessageType, pid: Pruefidentifikator) -> bool {
self.profiles_for(message_type)
.any(|p| p.ahb_rule_pack(Some(pid)).rule_count() > 0)
}
#[must_use]
pub fn releases(&self, message_type: MessageType) -> Vec<&Release> {
let mut releases: Vec<&Release> = self
.profiles
.iter()
.filter(|p| p.message_type() == message_type)
.map(|p| p.release())
.collect();
releases.sort();
releases.dedup_by_key(|r| r.as_str());
releases
}
#[must_use]
pub fn format_versions(&self) -> Vec<String> {
let mut dates: Vec<time::Date> = self
.profiles
.iter()
.filter_map(|p| p.valid_from())
.collect();
dates.sort();
dates.dedup();
dates
.into_iter()
.map(|d| format!("FV{:04}-{:02}-{:02}", d.year(), d.month() as u8, d.day()))
.collect()
}
#[must_use]
pub fn latest_for_track(
&self,
message_type: MessageType,
reference: &Release,
) -> Option<&Release> {
let kind = reference.kind();
let mut releases: Vec<&Release> = self
.profiles
.iter()
.filter(|p| p.message_type() == message_type && p.release().kind().same_track(&kind))
.map(|p| p.release())
.collect();
releases.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
releases.into_iter().next_back()
}
#[must_use]
pub fn profile_for_date_and_track(
&self,
message_type: MessageType,
date: time::Date,
track: &crate::release::ReleaseTrack,
) -> Option<&dyn Profile> {
self.profiles
.iter()
.filter(|p| p.message_type() == message_type)
.filter(|p| &p.release().track() == track)
.filter_map(|p| p.valid_from().map(|vf| (vf, *p)))
.filter(|(vf, _)| *vf <= date)
.max_by(|(va, pa), (vb, pb)| {
va.cmp(vb)
.then(pa.release().as_str().cmp(pb.release().as_str()))
})
.map(|(_, p)| p)
}
#[must_use]
pub fn is_acceptable_on(
&self,
message_type: MessageType,
release: &Release,
date: time::Date,
) -> bool {
let Ok(profile) = self.profile_on(message_type, release, date) else {
return false;
};
let Some(valid_from) = profile.valid_from() else {
return true;
};
if valid_from > date {
return false;
}
if let Some(valid_until) = profile.valid_until() {
let grace_end = valid_until + time::Duration::days(self.transition_grace_days);
if date > grace_end {
return false;
}
}
true
}
#[must_use]
pub fn transition_state(
&self,
message_type: MessageType,
date: time::Date,
track_prefix: Option<&str>,
) -> TransitionState<'_> {
let candidates: Vec<&dyn Profile> = self
.profiles
.iter()
.filter(|p| p.message_type() == message_type)
.filter(|p| track_prefix.is_none_or(|pfx| p.release().as_str().starts_with(pfx)))
.copied()
.collect();
let current = candidates
.iter()
.filter_map(|p| p.valid_from().map(|vf| (vf, *p)))
.filter(|(vf, _)| *vf <= date)
.max_by(|(va, pa), (vb, pb)| {
va.cmp(vb)
.then(pa.release().as_str().cmp(pb.release().as_str()))
})
.map(|(_, p)| p);
let Some(current) = current else {
return TransitionState::None;
};
let in_outgoing_grace = current.valid_until().is_some_and(|vu| {
let grace_end = vu + time::Duration::days(self.transition_grace_days);
date >= vu && date <= grace_end
});
if !in_outgoing_grace {
return TransitionState::Stable { profile: current };
}
let current_valid_from = current.valid_from().unwrap();
let incoming = candidates
.iter()
.filter_map(|p| p.valid_from().map(|vf| (vf, *p)))
.filter(|(vf, _)| *vf > current_valid_from)
.filter(|(vf, _)| {
let earliest = *vf - time::Duration::days(self.transition_grace_days);
date >= earliest
})
.min_by_key(|(vf, _)| *vf)
.map(|(_, p)| p);
match incoming {
Some(incoming) => TransitionState::Transition {
outgoing: current,
incoming,
},
None => TransitionState::Stable { profile: current },
}
}
}
#[derive(Clone)]
pub struct ProcessContext {
date: time::Date,
registry: std::sync::Arc<ReleaseRegistry>,
}
impl ProcessContext {
#[must_use]
pub fn for_date(date: time::Date) -> Self {
Self {
date,
registry: std::sync::Arc::clone(ReleaseRegistry::global_arc()),
}
}
#[must_use]
pub fn for_date_with_registry(
date: time::Date,
registry: std::sync::Arc<ReleaseRegistry>,
) -> Self {
Self { date, registry }
}
#[must_use]
pub fn current() -> Self {
Self::for_date(time::OffsetDateTime::now_utc().date())
}
#[must_use]
pub fn date(&self) -> time::Date {
self.date
}
#[must_use]
pub fn active_release(&self, message_type: MessageType) -> Option<&Release> {
self.registry
.profiles_for(message_type)
.filter_map(|p| p.valid_from().map(|vf| (vf, p)))
.filter(|(vf, _)| *vf <= self.date)
.max_by(|(va, pa), (vb, pb)| {
va.cmp(vb)
.then(pa.release().as_str().cmp(pb.release().as_str()))
})
.map(|(_, p)| p.release())
}
#[must_use]
pub fn active_profile(&self, message_type: MessageType) -> Option<&dyn Profile> {
self.registry
.profiles_for(message_type)
.filter_map(|p| p.valid_from().map(|vf| (vf, p)))
.filter(|(vf, _)| *vf <= self.date)
.max_by(|(va, pa), (vb, pb)| {
va.cmp(vb)
.then(pa.release().as_str().cmp(pb.release().as_str()))
})
.map(|(_, p)| p)
}
#[must_use]
pub fn active_release_for_track(
&self,
message_type: MessageType,
track: &crate::release::ReleaseTrack,
) -> Option<&Release> {
self.registry
.profile_for_date_and_track(message_type, self.date, track)
.map(Profile::release)
}
#[must_use]
pub fn active_profile_for_track(
&self,
message_type: MessageType,
track: &crate::release::ReleaseTrack,
) -> Option<&dyn Profile> {
self.registry
.profile_for_date_and_track(message_type, self.date, track)
}
#[must_use]
pub fn transition_state(
&self,
message_type: MessageType,
track_prefix: Option<&str>,
) -> TransitionState<'_> {
self.registry
.transition_state(message_type, self.date, track_prefix)
}
#[must_use]
pub fn is_acceptable(&self, message_type: MessageType, release: &Release) -> bool {
self.registry
.is_acceptable_on(message_type, release, self.date)
}
}