use crate::report::EdiEnergyReport;
#[cfg(any_message)]
pub(crate) fn sanitize_code(s: &str) -> String {
const MAX_CHARS: usize = 16;
s.chars()
.take(MAX_CHARS)
.map(|c| {
if c.is_ascii_alphanumeric() || c == '.' {
c
} else {
'?'
}
})
.collect()
}
#[cfg(all(test, any_message))]
mod sanitize_tests {
use super::sanitize_code;
#[test]
fn truncates_on_character_boundaries() {
assert_eq!(sanitize_code(&"ü".repeat(17)), "?".repeat(16));
}
#[test]
fn neutralises_escape_sequences_and_keeps_plain_codes() {
assert_eq!(sanitize_code("UTILMD"), "UTILMD");
assert_eq!(sanitize_code("5.5.3a"), "5.5.3a");
assert_eq!(sanitize_code("A\u{1b}[31mB"), "A??31mB");
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProfileError {
#[error("profile field `{field}` is mandatory but was not provided")]
MissingField {
field: std::borrow::Cow<'static, str>,
},
#[error("profile field `{field}` has invalid value {value:?}: {reason}")]
InvalidField {
field: &'static str,
value: String,
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("EDIFACT parse error: {0}")]
Parse(#[from] edifact_rs::EdifactError),
#[error("EDIFACT serialization error: {0}")]
Serialize(String),
#[error("message type {message_type:?} requires the disabled `{feature}` Cargo feature")]
FeatureNotEnabled {
message_type: String,
feature: String,
},
#[error("unknown EDIFACT message type code (check UNH DE 0065 element 1 component 0)")]
UnknownMessageType {
raw_code: String,
},
#[error("required segment {0} is missing")]
MissingSegment(&'static str),
#[error("malformed segment {0}")]
MalformedSegment(&'static str),
#[error("Pruefidentifikator not found in BGM segment")]
MissingPruefidentifikator,
#[error("invalid Pruefidentifikator {0}: must be a 5-digit code in the range 10000–99999")]
InvalidPruefidentifikatorRange(u32),
#[error("Pruefidentifikator field is not a decimal integer (non-numeric content in BGM)")]
InvalidPruefidentifikatorFormat {
raw_value: String,
},
#[error("release code is missing in UNH segment")]
MissingRelease,
#[error("invalid release code: {0}")]
InvalidRelease(&'static str),
#[error("no profile found for message type {message_type:?} release {release}")]
ProfileNotFound {
message_type: crate::MessageType,
release: crate::Release,
},
#[error(
"profile for {message_type:?} release {release} is not yet active on {date} (valid from {valid_from})"
)]
ProfileNotYetActive {
message_type: crate::MessageType,
release: crate::Release,
valid_from: time::Date,
date: time::Date,
},
#[error("validation failed with {count} error(s): {report}")]
Validation {
count: usize,
report: EdiEnergyReport,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("profile configuration error: {0}")]
Profile(#[from] ProfileError),
#[error(
"interchange UNZ count mismatch: UNZ declared {declared} message(s) but {actual} were found"
)]
InterchangeCountMismatch {
declared: usize,
actual: usize,
},
#[error("interchange control reference mismatch: UNB has {unb_ref:?} but UNZ has {unz_ref:?}")]
InterchangeRefMismatch {
unb_ref: String,
unz_ref: String,
},
#[error(
"interchange party mismatch: UNB {qualifier} is {unb_id:?} but NAD+{nad_qualifier} \
is {nad_id:?} (message {message_index}) — BDEW Allgemeine Festlegungen §2.13 \
requires them to be identical"
)]
InterchangePartyMismatch {
qualifier: &'static str,
nad_qualifier: &'static str,
unb_id: String,
nad_id: String,
message_index: usize,
},
#[error("interchange exceeds the maximum allowed message count of {limit}")]
TooManyMessages {
limit: usize,
},
#[error("message exceeds the maximum allowed segment count of {limit} (actual: {actual})")]
TooManySegmentsInMessage {
limit: usize,
actual: usize,
},
}
#[cfg(feature = "diagnostics")]
impl miette::Diagnostic for Error {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let code = match self {
Error::Parse(_) => "edi-energy::parse",
Error::Serialize(_) => "edi-energy::serialize",
Error::FeatureNotEnabled { .. } => "edi-energy::feature-not-enabled",
Error::UnknownMessageType { .. } => "edi-energy::unknown-message-type",
Error::MissingSegment(_) => "edi-energy::missing-segment",
Error::MalformedSegment(_) => "edi-energy::malformed-segment",
Error::MissingPruefidentifikator => "edi-energy::missing-pruefidentifikator",
Error::InvalidPruefidentifikatorRange(_)
| Error::InvalidPruefidentifikatorFormat { .. } => {
"edi-energy::invalid-pruefidentifikator"
}
Error::MissingRelease => "edi-energy::missing-release",
Error::InvalidRelease(_) => "edi-energy::invalid-release",
Error::ProfileNotFound { .. } => "edi-energy::profile-not-found",
Error::ProfileNotYetActive { .. } => "edi-energy::profile-not-yet-active",
Error::Validation { .. } => "edi-energy::validation",
Error::Io(_) => "edi-energy::io",
Error::Profile(_) => "edi-energy::profile-config",
Error::InterchangeCountMismatch { .. } => "edi-energy::interchange-count-mismatch",
Error::InterchangeRefMismatch { .. } => "edi-energy::interchange-ref-mismatch",
Error::InterchangePartyMismatch { .. } => "edi-energy::interchange-party-mismatch",
Error::TooManyMessages { .. } => "edi-energy::too-many-messages",
Error::TooManySegmentsInMessage { .. } => "edi-energy::too-many-segments-in-message",
};
Some(Box::new(code))
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
match self {
Error::FeatureNotEnabled {
message_type,
feature,
} => Some(Box::new(format!(
"add `{feature}` to the `[features]` section of your Cargo.toml to parse {message_type} messages"
))),
Error::ProfileNotFound {
message_type,
release,
} => Some(Box::new(format!(
"name {message_type} {release} in profiles/sources.json and run \
`cargo xtask import-profiles`"
))),
Error::ProfileNotYetActive {
message_type,
release,
valid_from,
date,
} => Some(Box::new(format!(
"{message_type} {release} is valid from {valid_from}; use a processing date ≥ {valid_from} (requested: {date})"
))),
Error::UnknownMessageType { raw_code } => Some(Box::new(format!(
"`{raw_code}` is not a recognised EDI@Energy message type"
))),
_ => None,
}
}
fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
match self {
Error::Parse(e) => Some(e as &dyn miette::Diagnostic),
_ => None,
}
}
}