use std::collections::HashSet;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Marktrolle {
Nb,
Lf,
Msb,
Nmsb,
Amsb,
Bkv,
Uenb,
Biko,
Esa,
Gnb,
Lfg,
Lfn,
Lfa,
Mgv,
}
impl Marktrolle {
#[must_use]
pub const fn as_code(self) -> &'static str {
match self {
Self::Nb => "NB",
Self::Lf => "LF",
Self::Msb => "MSB",
Self::Nmsb => "NMSB",
Self::Amsb => "AMSB",
Self::Bkv => "BKV",
Self::Uenb => "ÜNB",
Self::Biko => "BIKO",
Self::Esa => "ESA",
Self::Gnb => "GNB",
Self::Lfg => "LFG",
Self::Lfn => "LFN",
Self::Lfa => "LFA",
Self::Mgv => "MGV",
}
}
#[must_use]
pub fn from_code(code: &str) -> Option<Self> {
Some(match code {
"NB" => Self::Nb,
"LF" => Self::Lf,
"MSB" => Self::Msb,
"NMSB" => Self::Nmsb,
"AMSB" => Self::Amsb,
"BKV" => Self::Bkv,
"ÜNB" => Self::Uenb,
"BIKO" => Self::Biko,
"ESA" => Self::Esa,
"GNB" => Self::Gnb,
"LFG" => Self::Lfg,
"LFN" => Self::Lfn,
"LFA" => Self::Lfa,
"MGV" => Self::Mgv,
_ => return None,
})
}
#[must_use]
pub fn from_partin_pid(pid: u32) -> Option<Self> {
match pid {
37000 => Some(Self::Lf),
37001 => Some(Self::Nb),
37002 | 37010 | 37013 | 37014 => Some(Self::Msb),
37003 => Some(Self::Bkv),
37004 => Some(Self::Biko),
37005 => Some(Self::Uenb),
37006 => Some(Self::Esa),
37008 => Some(Self::Lfg),
37009 | 37012 => Some(Self::Gnb),
37011 => Some(Self::Mgv),
_ => None,
}
}
}
impl serde::Serialize for Marktrolle {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_code())
}
}
impl<'de> serde::Deserialize<'de> for Marktrolle {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let code = String::deserialize(deserializer)?;
Self::from_code(&code)
.ok_or_else(|| serde::de::Error::custom(format!("unknown Marktrolle code {code:?}")))
}
}
impl std::fmt::Display for Marktrolle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_code())
}
}
#[derive(Debug, Clone)]
pub struct DeploymentRoles {
all: bool,
roles: HashSet<Marktrolle>,
}
impl Default for DeploymentRoles {
fn default() -> Self {
Self::all()
}
}
impl DeploymentRoles {
#[must_use]
pub fn all() -> Self {
Self {
all: true,
roles: HashSet::new(),
}
}
#[must_use]
pub fn from_roles(roles: impl IntoIterator<Item = Marktrolle>) -> Self {
Self {
all: false,
roles: roles.into_iter().collect(),
}
}
#[must_use]
pub fn contains(&self, role: Marktrolle) -> bool {
self.all || self.roles.contains(&role)
}
#[must_use]
pub fn is_all(&self) -> bool {
self.all
}
#[must_use]
pub fn nb() -> Self {
Self::from_roles([Marktrolle::Nb])
}
#[must_use]
pub fn esa() -> Self {
Self::from_roles([Marktrolle::Esa])
}
#[must_use]
pub fn lf() -> Self {
Self::from_roles([Marktrolle::Lf])
}
#[must_use]
pub fn msb() -> Self {
Self::from_roles([Marktrolle::Msb])
}
#[must_use]
pub fn nmsb() -> Self {
Self::from_roles([Marktrolle::Nmsb])
}
#[must_use]
pub fn nb_msb() -> Self {
Self::from_roles([Marktrolle::Nb, Marktrolle::Msb])
}
#[must_use]
pub fn nb_bkv() -> Self {
Self::from_roles([Marktrolle::Nb, Marktrolle::Bkv])
}
#[must_use]
pub fn with(mut self, role: Marktrolle) -> Self {
if !self.all {
self.roles.insert(role);
}
self
}
}
impl FromIterator<Marktrolle> for DeploymentRoles {
fn from_iter<T: IntoIterator<Item = Marktrolle>>(iter: T) -> Self {
Self::from_roles(iter)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LicensingError {
MarktrolleRequired,
RoleNotPermitted,
RoleNotConfigured,
}
impl std::fmt::Display for LicensingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MarktrolleRequired => {
f.write_str("multi-role command requires an asserted Marktrolle")
}
Self::RoleNotPermitted => {
f.write_str("asserted Marktrolle is not permitted for this command")
}
Self::RoleNotConfigured => {
f.write_str("deployment is not configured for the required Marktrolle")
}
}
}
}
impl std::error::Error for LicensingError {}
pub fn resolve_role(
permitted: &[Marktrolle],
asserted: Option<Marktrolle>,
configured: &DeploymentRoles,
) -> Result<Marktrolle, LicensingError> {
let effective = if permitted.len() == 1 {
permitted[0]
} else {
let r = asserted.ok_or(LicensingError::MarktrolleRequired)?;
if !permitted.contains(&r) {
return Err(LicensingError::RoleNotPermitted);
}
r
};
if !configured.contains(effective) {
return Err(LicensingError::RoleNotConfigured);
}
Ok(effective)
}
#[cfg(test)]
mod licensing_tests {
use super::*;
#[test]
fn code_round_trip_for_every_role() {
for role in [
Marktrolle::Nb,
Marktrolle::Lf,
Marktrolle::Msb,
Marktrolle::Nmsb,
Marktrolle::Amsb,
Marktrolle::Bkv,
Marktrolle::Uenb,
Marktrolle::Biko,
Marktrolle::Esa,
Marktrolle::Gnb,
Marktrolle::Lfg,
Marktrolle::Lfn,
Marktrolle::Lfa,
Marktrolle::Mgv,
] {
assert_eq!(Marktrolle::from_code(role.as_code()), Some(role));
}
assert_eq!(Marktrolle::from_code("ÜNB"), Some(Marktrolle::Uenb));
assert_eq!(
Marktrolle::from_code("nb"),
None,
"codes are case-sensitive"
);
assert_eq!(Marktrolle::from_code(""), None);
}
#[test]
fn serde_round_trips_as_bdew_code() {
for role in [Marktrolle::Nb, Marktrolle::Uenb, Marktrolle::Lfg] {
let json = serde_json::to_string(&role).unwrap();
assert_eq!(json, format!("\"{}\"", role.as_code()));
let back: Marktrolle = serde_json::from_str(&json).unwrap();
assert_eq!(back, role);
}
assert!(serde_json::from_str::<Marktrolle>("\"LfStrom\"").is_err());
}
#[test]
fn from_partin_pid_covers_all_partin_pids() {
for pid in [
37000u32, 37001, 37002, 37003, 37004, 37005, 37006, 37008, 37009, 37010, 37011, 37012,
37013, 37014,
] {
assert!(
Marktrolle::from_partin_pid(pid).is_some(),
"from_partin_pid({pid}) should return Some"
);
}
assert_eq!(Marktrolle::from_partin_pid(37000), Some(Marktrolle::Lf));
assert_eq!(Marktrolle::from_partin_pid(37008), Some(Marktrolle::Lfg));
assert_eq!(Marktrolle::from_partin_pid(37009), Some(Marktrolle::Gnb));
assert_eq!(Marktrolle::from_partin_pid(37011), Some(Marktrolle::Mgv));
assert_eq!(Marktrolle::from_partin_pid(37014), Some(Marktrolle::Msb));
assert_eq!(Marktrolle::from_partin_pid(37007), None);
assert_eq!(Marktrolle::from_partin_pid(0), None);
}
#[test]
fn single_permitted_infers_and_ignores_assertion() {
let configured = DeploymentRoles::lf();
assert_eq!(
resolve_role(&[Marktrolle::Lf], None, &configured),
Ok(Marktrolle::Lf)
);
assert_eq!(
resolve_role(&[Marktrolle::Lf], Some(Marktrolle::Nb), &configured),
Ok(Marktrolle::Lf)
);
}
#[test]
fn multi_permitted_requires_assertion() {
let permitted = [Marktrolle::Nb, Marktrolle::Msb];
let configured = DeploymentRoles::nb_msb();
assert_eq!(
resolve_role(&permitted, None, &configured),
Err(LicensingError::MarktrolleRequired)
);
assert_eq!(
resolve_role(&permitted, Some(Marktrolle::Msb), &configured),
Ok(Marktrolle::Msb)
);
}
#[test]
fn multi_permitted_rejects_foreign_assertion() {
let permitted = [Marktrolle::Nb, Marktrolle::Msb];
let configured = DeploymentRoles::lf();
assert_eq!(
resolve_role(&permitted, Some(Marktrolle::Lf), &configured),
Err(LicensingError::RoleNotPermitted)
);
}
#[test]
fn configured_cross_check_rejects_unconfigured_role() {
assert_eq!(
resolve_role(&[Marktrolle::Lf], None, &DeploymentRoles::nb()),
Err(LicensingError::RoleNotConfigured)
);
assert_eq!(
resolve_role(&[Marktrolle::Lf], None, &DeploymentRoles::from_roles([])),
Err(LicensingError::RoleNotConfigured)
);
}
#[test]
fn deployment_roles_all_admits_every_role() {
assert_eq!(
resolve_role(&[Marktrolle::Biko], None, &DeploymentRoles::all()),
Ok(Marktrolle::Biko)
);
assert_eq!(
resolve_role(
&[Marktrolle::Bkv, Marktrolle::Uenb],
Some(Marktrolle::Uenb),
&DeploymentRoles::all()
),
Ok(Marktrolle::Uenb)
);
}
}
#[cfg(test)]
mod esa_role_tests {
use super::*;
#[test]
fn esa_is_a_selectable_deployment_role() {
let roles = DeploymentRoles::esa();
assert!(roles.contains(Marktrolle::Esa));
assert!(!roles.contains(Marktrolle::Msb));
assert!(!roles.is_all());
}
#[test]
fn msb_and_esa_can_be_held_together() {
let roles = DeploymentRoles::from_roles([Marktrolle::Msb, Marktrolle::Esa]);
assert!(roles.contains(Marktrolle::Msb));
assert!(roles.contains(Marktrolle::Esa));
}
}