pub mod conditions;
pub mod model;
pub mod skeleton;
pub mod structure;
pub mod validate;
use std::collections::HashMap;
use std::fmt;
use edifact_rs::{GroupDef, Segment, ValidationIssue};
use crate::registry::PidSource;
use crate::{MessageType, ProfileError, Pruefidentifikator, Release};
pub use model::{
AhbProfile, Anwendungsfall, Element, ElementRule, MigProfile, Operand, Row, SegmentNode,
};
pub use skeleton::SkeletonParties;
pub use structure::{Resolution, Structure};
pub struct Profile {
pub mig: MigProfile,
pub ahb: AhbProfile,
pub structure: Structure,
message_type: MessageType,
release: Release,
valid_from: Option<time::Date>,
valid_until: Option<time::Date>,
by_pid: HashMap<u32, usize>,
group_schema: &'static [GroupDef<'static>],
}
impl fmt::Debug for Profile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Profile")
.field("message_type", &self.message_type)
.field("release", &self.release.as_str())
.field("valid_from", &self.valid_from)
.field("anwendungsfaelle", &self.ahb.anwendungsfaelle.len())
.finish_non_exhaustive()
}
}
impl Profile {
pub fn from_json(mig: &str, ahb: &str) -> Result<Self, ProfileError> {
let mig: MigProfile =
serde_json::from_str(mig).map_err(|e| ProfileError::InvalidField {
field: "mig.json",
value: String::new(),
reason: e.to_string(),
})?;
let ahb: AhbProfile =
serde_json::from_str(ahb).map_err(|e| ProfileError::InvalidField {
field: "ahb.json",
value: String::new(),
reason: e.to_string(),
})?;
Self::new(mig, ahb)
}
pub fn new(mig: MigProfile, ahb: AhbProfile) -> Result<Self, ProfileError> {
if mig.message_type != ahb.message_type || mig.release != ahb.release {
return Err(ProfileError::InvalidField {
field: "release",
value: format!(
"{} {} / {} {}",
mig.message_type, mig.release, ahb.message_type, ahb.release
),
reason: "mig.json and ahb.json describe different documents".into(),
});
}
let message_type = MessageType::from_unh_code(&mig.message_type).ok_or_else(|| {
ProfileError::InvalidField {
field: "message_type",
value: mig.message_type.clone(),
reason: "not an EDI@Energy message type".into(),
}
})?;
let date =
|field: &'static str, s: &Option<String>| -> Result<Option<time::Date>, ProfileError> {
match s {
None => Ok(None),
Some(s) => {
time::Date::parse(s, &time::format_description::well_known::Iso8601::DATE)
.map(Some)
.map_err(|e| ProfileError::InvalidField {
field,
value: s.clone(),
reason: e.to_string(),
})
}
}
};
let valid_from = date("valid_from", &Some(mig.valid_from.clone()))?;
let valid_until = date("valid_until", &mig.valid_until)?;
let structure = Structure::compile(&mig);
let by_pid = ahb
.anwendungsfaelle
.iter()
.enumerate()
.filter_map(|(i, a)| a.pid.map(|p| (p, i)))
.collect();
let group_schema = leak_group_schema(&structure);
Ok(Self {
release: Release::new(&mig.release),
message_type,
valid_from,
valid_until,
by_pid,
group_schema,
structure,
mig,
ahb,
})
}
#[must_use]
pub fn message_type(&self) -> MessageType {
self.message_type
}
#[must_use]
pub fn release(&self) -> &Release {
&self.release
}
#[must_use]
pub fn valid_from(&self) -> Option<time::Date> {
self.valid_from
}
#[must_use]
pub fn valid_until(&self) -> Option<time::Date> {
self.valid_until
}
#[must_use]
pub fn ahb_version(&self) -> &str {
&self.ahb.ahb_version
}
#[must_use]
pub fn source_document(&self) -> Option<&str> {
self.ahb.source.title.as_deref()
}
#[must_use]
pub fn pid_source(&self) -> PidSource {
match self.mig.pid_source.as_deref() {
Some("rff_z13") => PidSource::RffZ13,
_ => PidSource::BgmDe1004,
}
}
#[must_use]
pub fn pid_exempt(&self) -> bool {
self.mig.pid_exempt || self.ahb.anwendungsfaelle.iter().all(|a| a.pid.is_none())
}
#[must_use]
pub fn anwendungsfaelle(&self) -> &[Anwendungsfall] {
&self.ahb.anwendungsfaelle
}
#[must_use]
pub fn anwendungsfall(&self, pid: u32) -> Option<&Anwendungsfall> {
self.by_pid
.get(&pid)
.map(|&i| &self.ahb.anwendungsfaelle[i])
}
#[must_use]
pub fn has_anwendungsfall(&self, pid: Pruefidentifikator) -> bool {
self.by_pid.contains_key(&pid.as_u32())
}
#[must_use]
pub fn pruefidentifikatoren(&self) -> Vec<u32> {
let mut v: Vec<u32> = self.by_pid.keys().copied().collect();
v.sort_unstable();
v
}
#[must_use]
pub fn validate(
&self,
segments: &[Segment<'_>],
pid: Option<Pruefidentifikator>,
) -> Vec<ValidationIssue> {
validate::validate(self, segments, pid.map(Pruefidentifikator::as_u32))
}
#[must_use]
pub fn resolve(&self, segments: &[Segment<'_>]) -> Resolution {
self.structure.resolve(segments)
}
#[must_use]
pub fn pruefschablone(&self, pid: u32) -> Option<Pruefschablone<'_>> {
let af = self.anwendungsfall(pid)?;
Some(Pruefschablone::new(self, af))
}
#[must_use]
pub fn group_schema(&self) -> &'static [GroupDef<'static>] {
self.group_schema
}
}
fn leak_group_schema(structure: &Structure) -> &'static [GroupDef<'static>] {
fn build(structure: &Structure, ids: &[structure::NodeId]) -> &'static [GroupDef<'static>] {
let mut out: Vec<GroupDef<'static>> = Vec::new();
for &id in ids {
let Some(group) = structure.group(id) else {
continue;
};
let Some(trigger) = structure.trigger(id).and_then(|t| structure.tag(t)) else {
continue;
};
if out.iter().any(|g| g.name == group && g.trigger == trigger) {
continue;
}
let name: &'static str = Box::leak(group.to_owned().into_boxed_str());
let trigger: &'static str = Box::leak(trigger.to_owned().into_boxed_str());
let children = build(structure, &structure.nodes[id].children);
out.push(if children.is_empty() {
GroupDef::new(name, trigger)
} else {
GroupDef::with_children(name, trigger, children)
});
}
Box::leak(out.into_boxed_slice())
}
build(structure, &structure.root)
}
#[derive(Debug)]
pub struct Pruefschablone<'p> {
pub pid: Option<u32>,
pub name: &'p str,
pub communication: Option<&'p str>,
pub chapter: Option<&'p str>,
pub rows: Vec<SchablonenZeile<'p>>,
}
#[derive(Debug)]
pub struct SchablonenZeile<'p> {
pub path: Vec<&'p str>,
pub nr: &'p str,
pub tag: &'p str,
pub name: &'p str,
pub group_status: Option<&'p [String]>,
pub status: &'p [String],
pub elements: Vec<ElementZeile<'p>>,
}
#[derive(Debug)]
pub struct ElementZeile<'p> {
pub de: &'p str,
pub name: &'p str,
pub occurrence: u8,
pub operands: &'p [Operand],
}
impl<'p> Pruefschablone<'p> {
fn new(profile: &'p Profile, af: &'p Anwendungsfall) -> Self {
let s = &profile.structure;
let mut rows = Vec::new();
for (id, node) in s.nodes.iter().enumerate() {
let structure::Kind::Segment {
nr, tag, layout, ..
} = &node.kind
else {
continue;
};
let Some(status) = af.segment_status(nr) else {
continue;
};
let seg = &s.layouts[*layout];
let group_status = node
.parent
.and_then(|p| s.group(p).map(|g| (g, p)))
.filter(|(_, p)| s.trigger(*p) == Some(id))
.and_then(|(g, _)| af.group_status(g, nr));
let elements = af
.element_rules(nr)
.filter_map(|r| {
seg.locate(&r.de, r.occurrence)
.map(|(_, _, el)| ElementZeile {
de: r.de.as_str(),
name: el.name.as_str(),
occurrence: r.occurrence,
operands: r.operands.as_slice(),
})
})
.collect();
rows.push(SchablonenZeile {
path: s.path(id),
nr,
tag,
name: &node.name,
group_status,
status,
elements,
});
}
Self {
pid: af.pid,
name: &af.name,
communication: af.communication.as_deref(),
chapter: af.chapter.as_deref(),
rows,
}
}
}
impl fmt::Display for Pruefschablone<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.pid {
Some(p) => writeln!(f, "{p} {}", self.name)?,
None => writeln!(f, "{}", self.name)?,
}
if let Some(c) = self.communication {
writeln!(f, "{c}")?;
}
for row in &self.rows {
let indent = " ".repeat(row.path.len());
let group = row.path.last().map_or(String::new(), |g| format!("{g} "));
if let Some(gs) = row.group_status {
writeln!(f, "{indent}{group:<5}{:<32}{}", "", gs.join(" | "))?;
}
writeln!(
f,
"{indent}{group}{} {} {:<24} {}",
row.tag,
row.nr,
truncate(row.name, 24),
row.status.join(" | ")
)?;
for el in &row.elements {
let ops: Vec<String> = el
.operands
.iter()
.map(|o| match &o.code {
Some(c) => format!("{c}={}", o.operand),
None => o.operand.clone(),
})
.collect();
writeln!(
f,
"{indent} {} {:<24} {}",
el.de,
truncate(el.name, 24),
ops.join(", ")
)?;
}
}
Ok(())
}
}
fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
s.to_owned()
} else {
let mut t: String = s.chars().take(n - 1).collect();
t.push('…');
t
}
}