use std::sync::OnceLock;
pub use crate::profile::Profile;
use crate::{Error, MessageType, Pruefidentifikator, Release};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PidSource {
#[default]
BgmDe1004,
RffZ13,
}
pub const DEFAULT_RECEIVE_TOLERANCE_DAYS: i64 = 0;
pub enum TransitionState<'r> {
Stable {
profile: &'r Profile,
},
Transition {
outgoing: &'r Profile,
incoming: &'r 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 Profile>,
index: std::collections::HashMap<MessageType, std::collections::HashMap<Box<str>, Vec<usize>>>,
receive_tolerance_days: i64,
}
static REGISTRY: OnceLock<std::sync::Arc<ReleaseRegistry>> = OnceLock::new();
impl ReleaseRegistry {
#[must_use]
pub fn new(profiles: Vec<&'static 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,
receive_tolerance_days: DEFAULT_RECEIVE_TOLERANCE_DAYS,
}
}
#[must_use]
pub fn with_receive_tolerance_days(mut self, days: i64) -> Self {
self.receive_tolerance_days = days;
self
}
#[must_use]
pub fn receive_tolerance_days(&self) -> i64 {
self.receive_tolerance_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 Profile> = Vec::new();
crate::register_profiles(&mut profiles);
std::sync::Arc::new(Self::new(profiles))
})
}
pub fn profile(
&self,
message_type: MessageType,
release: &Release,
) -> Result<&'static Profile, Error> {
self.profile_on(message_type, release, time::Date::MAX)
}
pub fn profile_on(
&self,
message_type: MessageType,
release: &Release,
date: time::Date,
) -> Result<&'static Profile, Error> {
let candidates = self
.index
.get(&message_type)
.and_then(|m| m.get(release.as_str()))
.ok_or_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 Profile] {
&self.profiles
}
pub fn profiles_for(
&self,
message_type: MessageType,
) -> impl Iterator<Item = &'static 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.has_anwendungsfall(pid))
}
#[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<&'static 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 date < valid_from {
return false;
}
if let Some(valid_until) = profile.valid_until() {
let tolerance_end = valid_until + time::Duration::days(self.receive_tolerance_days);
if date > tolerance_end {
return false;
}
}
true
}
#[must_use]
pub fn transition_state(
&self,
message_type: MessageType,
date: time::Date,
track: Option<crate::ReleaseTrack>,
) -> TransitionState<'_> {
let candidates: Vec<&Profile> = self
.profiles
.iter()
.filter(|p| p.message_type() == message_type)
.filter(|p| track.is_none_or(|t| p.release().track() == t))
.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 tolerance = time::Duration::days(self.receive_tolerance_days);
let current_valid_from = current.valid_from().unwrap_or(date);
let outgoing = candidates
.iter()
.filter(|p| p.valid_from().is_some_and(|vf| vf < current_valid_from))
.filter(|p| {
p.valid_until()
.is_some_and(|vu| vu < date && date <= vu + tolerance)
})
.max_by_key(|p| p.valid_from())
.copied();
match outgoing {
Some(outgoing) => TransitionState::Transition {
outgoing,
incoming: current,
},
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 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<&'static 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<&'static Profile> {
self.registry
.profile_for_date_and_track(message_type, self.date, track)
}
#[must_use]
pub fn transition_state(
&self,
message_type: MessageType,
track: Option<crate::ReleaseTrack>,
) -> TransitionState<'_> {
self.registry
.transition_state(message_type, self.date, track)
}
#[must_use]
pub fn is_acceptable(&self, message_type: MessageType, release: &Release) -> bool {
self.registry
.is_acceptable_on(message_type, release, self.date)
}
}