use std::fmt;
use std::str::FromStr;
use crate::constants;
pub use asx_rs::as4::pmode::{MepType, PMode, PModeRegistry, PModeSecurity, PayloadPackagingMode};
pub use asx_rs::crypto::wssec::WsSecOutboundKeyInfoProfile;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BdewAction {
Utilmd,
Aperak,
Contrl,
Mscons,
Invoic,
Remadv,
Iftsta,
Ordrsp,
Orders,
Ordchg,
Reqote,
Insrpt,
Pricat,
Quotes,
Partin,
Utilts,
Custom(String),
}
impl BdewAction {
#[must_use]
pub fn custom(type_name: impl Into<String>) -> Self {
Self::Custom(format!(
"{}:{}",
crate::constants::SERVICE,
type_name.into()
))
}
#[must_use]
pub fn all_standard() -> Vec<Self> {
vec![
Self::Utilmd,
Self::Aperak,
Self::Contrl,
Self::Mscons,
Self::Invoic,
Self::Remadv,
Self::Iftsta,
Self::Ordrsp,
Self::Orders,
Self::Ordchg,
Self::Reqote,
Self::Insrpt,
Self::Pricat,
Self::Quotes,
Self::Partin,
Self::Utilts,
]
}
#[must_use]
pub fn as_edifact_type(&self) -> &str {
match self {
Self::Utilmd => "UTILMD",
Self::Aperak => "APERAK",
Self::Contrl => "CONTRL",
Self::Mscons => "MSCONS",
Self::Invoic => "INVOIC",
Self::Remadv => "REMADV",
Self::Iftsta => "IFTSTA",
Self::Ordrsp => "ORDRSP",
Self::Orders => "ORDERS",
Self::Ordchg => "ORDCHG",
Self::Reqote => "REQOTE",
Self::Insrpt => "INSRPT",
Self::Pricat => "PRICAT",
Self::Quotes => "QUOTES",
Self::Partin => "PARTIN",
Self::Utilts => "UTILTS",
Self::Custom(uri) => uri.as_str(),
}
}
#[must_use]
pub fn as_uri(&self) -> String {
match self {
Self::Custom(uri) => uri.clone(),
_ => format!("{}:{}", constants::SERVICE, self.as_edifact_type()),
}
}
}
impl fmt::Display for BdewAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_edifact_type())
}
}
impl FromStr for BdewAction {
type Err = ParseBdewActionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let action = match s {
"UTILMD" => Self::Utilmd,
"APERAK" => Self::Aperak,
"CONTRL" => Self::Contrl,
"MSCONS" => Self::Mscons,
"INVOIC" => Self::Invoic,
"REMADV" => Self::Remadv,
"IFTSTA" => Self::Iftsta,
"ORDRSP" => Self::Ordrsp,
"ORDERS" => Self::Orders,
"ORDCHG" => Self::Ordchg,
"REQOTE" => Self::Reqote,
"INSRPT" => Self::Insrpt,
"PRICAT" => Self::Pricat,
"QUOTES" => Self::Quotes,
"PARTIN" => Self::Partin,
"UTILTS" => Self::Utilts,
other => Self::custom(other),
};
Ok(action)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseBdewActionError {}
impl fmt::Display for ParseBdewActionError {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
unreachable!()
}
}
impl std::error::Error for ParseBdewActionError {}
#[must_use]
#[inline]
pub fn bdew_action_from_str(message_type: &str) -> BdewAction {
message_type
.parse::<BdewAction>()
.unwrap_or_else(|_| BdewAction::custom(message_type))
}
#[must_use]
pub fn bdew_pmode(
id: impl Into<String>,
partner_mp_id: impl Into<String>,
action: BdewAction,
) -> PMode {
PMode {
id: id.into(),
partner_id: partner_mp_id.into(),
service: constants::SERVICE.to_string(),
service_type: constants::SERVICE_TYPE.to_string(),
action: action.as_uri(),
mep: MepType::OneWayPush,
security: PModeSecurity {
sign: true,
encrypt: true,
encrypt_soap_headers: false,
compress: true,
outbound_key_info_profile: WsSecOutboundKeyInfoProfile::X509PKIPathv1,
},
payload_packaging: PayloadPackagingMode::MimeAttachment,
endpoint_url: None,
}
}
#[must_use]
pub fn bdew_pmode_with_endpoint(
id: impl Into<String>,
partner_mp_id: impl Into<String>,
action: BdewAction,
endpoint_url: impl Into<String>,
) -> PMode {
PMode {
endpoint_url: Some(endpoint_url.into()),
..bdew_pmode(id, partner_mp_id, action)
}
}
#[must_use]
pub fn bdew_pmode_sign_only(
id: impl Into<String>,
partner_mp_id: impl Into<String>,
action: BdewAction,
) -> PMode {
PMode {
security: PModeSecurity {
sign: true,
encrypt: false,
encrypt_soap_headers: false,
compress: true,
outbound_key_info_profile: WsSecOutboundKeyInfoProfile::X509PKIPathv1,
},
..bdew_pmode(id, partner_mp_id, action)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants;
#[test]
fn as_edifact_type_all_standard() {
assert_eq!(BdewAction::Utilmd.as_edifact_type(), "UTILMD");
assert_eq!(BdewAction::Aperak.as_edifact_type(), "APERAK");
assert_eq!(BdewAction::Contrl.as_edifact_type(), "CONTRL");
assert_eq!(BdewAction::Mscons.as_edifact_type(), "MSCONS");
assert_eq!(BdewAction::Invoic.as_edifact_type(), "INVOIC");
assert_eq!(BdewAction::Remadv.as_edifact_type(), "REMADV");
assert_eq!(BdewAction::Iftsta.as_edifact_type(), "IFTSTA");
assert_eq!(BdewAction::Ordrsp.as_edifact_type(), "ORDRSP");
assert_eq!(BdewAction::Orders.as_edifact_type(), "ORDERS");
assert_eq!(BdewAction::Ordchg.as_edifact_type(), "ORDCHG");
assert_eq!(BdewAction::Reqote.as_edifact_type(), "REQOTE");
assert_eq!(BdewAction::Insrpt.as_edifact_type(), "INSRPT");
assert_eq!(BdewAction::Pricat.as_edifact_type(), "PRICAT");
assert_eq!(BdewAction::Quotes.as_edifact_type(), "QUOTES");
assert_eq!(BdewAction::Partin.as_edifact_type(), "PARTIN");
assert_eq!(BdewAction::Utilts.as_edifact_type(), "UTILTS");
}
#[test]
fn as_uri_utilmd() {
assert_eq!(BdewAction::Utilmd.as_uri(), "urn:bdew:as4:service:UTILMD");
}
#[test]
fn as_uri_aperak() {
assert_eq!(BdewAction::Aperak.as_uri(), "urn:bdew:as4:service:APERAK");
}
#[test]
fn as_uri_partin() {
assert_eq!(BdewAction::Partin.as_uri(), "urn:bdew:as4:service:PARTIN");
}
#[test]
fn as_uri_utilts() {
assert_eq!(BdewAction::Utilts.as_uri(), "urn:bdew:as4:service:UTILTS");
}
#[test]
fn as_uri_custom_passthrough() {
let uri = "urn:custom:action:FOO";
assert_eq!(BdewAction::Custom(uri.to_string()).as_uri(), uri);
}
#[test]
fn all_standard_has_16_variants() {
assert_eq!(BdewAction::all_standard().len(), 16);
}
#[test]
fn all_standard_no_duplicates() {
let v = BdewAction::all_standard();
let uris: std::collections::HashSet<String> = v.iter().map(|a| a.as_uri()).collect();
assert_eq!(
uris.len(),
v.len(),
"all_standard() must not contain duplicate URIs"
);
}
#[test]
fn all_standard_contains_partin_and_utilts() {
let v = BdewAction::all_standard();
assert!(
v.contains(&BdewAction::Partin),
"all_standard must include Partin"
);
assert!(
v.contains(&BdewAction::Utilts),
"all_standard must include Utilts"
);
}
#[test]
fn custom_builds_full_uri() {
let action = BdewAction::custom("SLSFCT");
assert_eq!(action.as_uri(), "urn:bdew:as4:service:SLSFCT");
}
#[test]
fn display_shows_edifact_type_name() {
assert_eq!(BdewAction::Utilmd.to_string(), "UTILMD");
assert_eq!(BdewAction::Partin.to_string(), "PARTIN");
assert_eq!(BdewAction::Utilts.to_string(), "UTILTS");
}
#[test]
fn from_str_all_standard_roundtrip() {
for action in BdewAction::all_standard() {
let type_name = action.as_edifact_type();
let parsed: BdewAction = type_name.parse().unwrap();
assert_eq!(
parsed, action,
"from_str({type_name}) did not round-trip through as_edifact_type"
);
}
}
#[test]
fn from_str_partin() {
let a: BdewAction = "PARTIN".parse().unwrap();
assert_eq!(a, BdewAction::Partin);
}
#[test]
fn from_str_utilts() {
let a: BdewAction = "UTILTS".parse().unwrap();
assert_eq!(a, BdewAction::Utilts);
}
#[test]
fn from_str_unknown_maps_to_custom_not_error() {
let a: BdewAction = "SLSFCT".parse().unwrap();
assert!(
matches!(a, BdewAction::Custom(_)),
"Unknown type must map to Custom, not Err"
);
assert_eq!(a.as_uri(), "urn:bdew:as4:service:SLSFCT");
}
#[test]
fn bdew_action_from_str_helper() {
assert_eq!(bdew_action_from_str("UTILMD"), BdewAction::Utilmd);
assert_eq!(bdew_action_from_str("PARTIN"), BdewAction::Partin);
assert_eq!(bdew_action_from_str("UTILTS"), BdewAction::Utilts);
assert!(matches!(
bdew_action_from_str("UNKNOWN"),
BdewAction::Custom(_)
));
}
#[test]
fn bdew_pmode_defaults() {
let pm = bdew_pmode("pm-1", "9900000000001", BdewAction::Utilmd);
assert_eq!(pm.partner_id, "9900000000001");
assert_eq!(pm.service, constants::SERVICE);
assert_eq!(pm.service_type, "");
assert_eq!(pm.action, BdewAction::Utilmd.as_uri());
assert_eq!(pm.mep, MepType::OneWayPush);
assert!(pm.security.sign);
assert!(
pm.security.encrypt,
"BDEW AS4-Profil v1.2 §2.2.6.2.2 requires encryption"
);
assert!(
pm.security.compress,
"BDEW AS4-Profil v1.2 §2.2.3.2/§2.2.3.3 make AS4 compression mandatory — \
it is why the payload is binary in its own part and the SOAP Body is empty"
);
assert_eq!(pm.payload_packaging, PayloadPackagingMode::MimeAttachment);
assert!(
pm.endpoint_url.is_none(),
"bdew_pmode leaves endpoint_url unset"
);
}
#[test]
fn bdew_pmode_with_endpoint_sets_url() {
let url = "https://partner.example/as4/inbox";
let pm = bdew_pmode_with_endpoint("pm-1", "9900000000001", BdewAction::Utilmd, url);
assert_eq!(pm.endpoint_url.as_deref(), Some(url));
assert!(pm.security.sign);
assert!(
pm.security.encrypt,
"bdew_pmode_with_endpoint inherits encrypt:true from bdew_pmode"
);
}
#[test]
fn bdew_pmode_sign_only_disables_encrypt() {
let pm = bdew_pmode_sign_only("pm-dev", "9900000000001", BdewAction::Utilmd);
assert!(pm.security.sign);
assert!(!pm.security.encrypt, "sign_only must have encrypt:false");
assert_eq!(pm.mep, MepType::OneWayPush);
assert!(pm.endpoint_url.is_none());
}
#[test]
fn bdew_pmode_partin_action() {
let pm = bdew_pmode("pm-partin", "9900000000001", BdewAction::Partin);
assert_eq!(pm.action, "urn:bdew:as4:service:PARTIN");
}
#[test]
fn bdew_pmode_utilts_action() {
let pm = bdew_pmode("pm-utilts", "9900000000001", BdewAction::Utilts);
assert_eq!(pm.action, "urn:bdew:as4:service:UTILTS");
}
#[test]
fn pmode_registry_resolves_by_partner_and_action() {
let mut registry = PModeRegistry::new();
registry.register(bdew_pmode("pm-u", "9900000000001", BdewAction::Utilmd));
registry.register(bdew_pmode("pm-a", "9900000000001", BdewAction::Aperak));
let pm = registry.resolve(
"9900000000001",
constants::SERVICE,
&BdewAction::Utilmd.as_uri(),
);
assert!(pm.is_some());
assert_eq!(pm.unwrap().id, "pm-u");
assert!(
registry
.resolve(
"9900000000002",
constants::SERVICE,
&BdewAction::Utilmd.as_uri(),
)
.is_none()
);
}
#[test]
fn pmode_registry_for_all_standard_actions() {
let mut registry = PModeRegistry::new();
for action in BdewAction::all_standard() {
let id = format!("pm-{}-partner", action);
registry.register(bdew_pmode(id, "9900000000001", action.clone()));
}
assert!(
registry
.resolve(
"9900000000001",
constants::SERVICE,
&BdewAction::Partin.as_uri()
)
.is_some(),
"PARTIN P-Mode must resolve"
);
assert!(
registry
.resolve(
"9900000000001",
constants::SERVICE,
&BdewAction::Utilts.as_uri()
)
.is_some(),
"UTILTS P-Mode must resolve"
);
}
}