use std::borrow::Cow;
use std::marker::PhantomData;
use crate::current::ZusatzAttribut;
pub const SEPARATOR: char = ':';
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NamespaceError {
Empty,
ContainsSeparator,
InvalidCharacter {
position: usize,
character: char,
},
}
impl std::fmt::Display for NamespaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => f.write_str("a namespace prefix may not be empty"),
Self::ContainsSeparator => write!(
f,
"a namespace prefix may not contain the separator '{SEPARATOR}'"
),
Self::InvalidCharacter {
position,
character,
} => write!(
f,
"invalid character '{character}' at position {position}; \
a namespace prefix is [A-Za-z0-9_-]+"
),
}
}
}
impl std::error::Error for NamespaceError {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Namespace(Cow<'static, str>);
impl Namespace {
pub const MAKO: Self = Self(Cow::Borrowed("mako"));
pub const HEMS: Self = Self(Cow::Borrowed("hems"));
pub const EDMD: Self = Self(Cow::Borrowed("edmd"));
pub const MABIS: Self = Self(Cow::Borrowed("mabis"));
pub const REGISTERED: &'static [Self] = &[Self::MAKO, Self::HEMS, Self::EDMD, Self::MABIS];
pub fn new(prefix: &str) -> Result<Self, NamespaceError> {
Self::check(prefix)?;
Ok(Self(Cow::Owned(prefix.to_owned())))
}
pub fn from_static(prefix: &'static str) -> Result<Self, NamespaceError> {
Self::check(prefix)?;
Ok(Self(Cow::Borrowed(prefix)))
}
fn check(prefix: &str) -> Result<(), NamespaceError> {
if prefix.is_empty() {
return Err(NamespaceError::Empty);
}
for (position, character) in prefix.char_indices() {
if character == SEPARATOR {
return Err(NamespaceError::ContainsSeparator);
}
if !character.is_ascii_alphanumeric() && character != '_' && character != '-' {
return Err(NamespaceError::InvalidCharacter {
position,
character,
});
}
}
Ok(())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_registered(&self) -> bool {
Self::REGISTERED.contains(self)
}
#[must_use]
pub fn name(&self, key: &str) -> String {
let mut out = String::with_capacity(self.0.len() + 1 + key.len());
out.push_str(&self.0);
out.push(SEPARATOR);
out.push_str(key);
out
}
#[must_use]
pub fn split(name: &str) -> Option<(&str, &str)> {
let (prefix, key) = name.split_once(SEPARATOR)?;
(!prefix.is_empty()).then_some((prefix, key))
}
#[must_use]
pub fn key_of<'a>(&self, name: &'a str) -> Option<&'a str> {
let (ns, key) = Self::split(name)?;
(ns == self.0).then_some(key)
}
}
impl std::fmt::Display for Namespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{SEPARATOR}", self.0)
}
}
impl std::str::FromStr for Namespace {
type Err = NamespaceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s.strip_suffix(SEPARATOR).unwrap_or(s))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AttributKey<T: ?Sized> {
namespace: Namespace,
key: &'static str,
_value: PhantomData<fn() -> T>,
}
impl<T: ?Sized> AttributKey<T> {
#[must_use]
pub const fn new(namespace: Namespace, key: &'static str) -> Self {
Self {
namespace,
key,
_value: PhantomData,
}
}
#[must_use]
pub const fn namespace(&self) -> &Namespace {
&self.namespace
}
#[must_use]
pub const fn key(&self) -> &'static str {
self.key
}
#[must_use]
pub fn name(&self) -> String {
self.namespace.name(self.key)
}
}
impl<T: ?Sized> std::fmt::Display for AttributKey<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{SEPARATOR}{}", self.namespace.as_str(), self.key)
}
}
pub mod well_known {
use super::{AttributKey, Namespace};
pub const ZAEHLPUNKT: AttributKey<crate::identifiers::Zaehlpunkt> =
AttributKey::new(Namespace::MABIS, "zaehlpunkt");
}
pub trait HasZusatzAttribute {
fn zusatz_attribute_field(&self) -> Option<&Vec<ZusatzAttribut>>;
fn zusatz_attribute_field_mut(&mut self) -> &mut Option<Vec<ZusatzAttribut>>;
}
pub trait ZusatzAttributeExt: HasZusatzAttribute {
fn zusatz_attribute(&self) -> impl Iterator<Item = &ZusatzAttribut> {
self.zusatz_attribute_field()
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
}
fn zusatz_attribut(&self, name: &str) -> Option<&ZusatzAttribut> {
self.zusatz_attribute()
.find(|a| a.name.as_deref() == Some(name))
}
fn zusatz_attribut_in(&self, namespace: &Namespace, key: &str) -> Option<&ZusatzAttribut> {
self.zusatz_attribut(&namespace.name(key))
}
fn zusatz_attribute_in<'a>(
&'a self,
namespace: &'a Namespace,
) -> impl Iterator<Item = (&'a str, &'a ZusatzAttribut)> {
self.zusatz_attribute()
.filter_map(move |a| Some((namespace.key_of(a.name.as_deref()?)?, a)))
}
fn has_zusatz_attribute_in(&self, namespace: &Namespace) -> bool {
self.zusatz_attribute_in(namespace).next().is_some()
}
fn zusatz_attribut_namespaces(&self) -> Vec<&str> {
let mut out: Vec<&str> = Vec::new();
for a in self.zusatz_attribute() {
if let Some((ns, _)) = a.name.as_deref().and_then(Namespace::split) {
if !out.contains(&ns) {
out.push(ns);
}
}
}
out
}
fn zusatz_attribut_str(&self, name: &str) -> Option<&str> {
let attribut = self.zusatz_attribut(name)?;
#[cfg(feature = "json")]
{
attribut.wert.as_ref()?.as_str()
}
#[cfg(not(feature = "json"))]
{
attribut.wert.as_deref()
}
}
fn zusatz_attribut_str_in(&self, namespace: &Namespace, key: &str) -> Option<&str> {
self.zusatz_attribut_str(&namespace.name(key))
}
fn set_zusatz_attribut(
&mut self,
name: impl Into<String>,
wert: impl Into<String>,
) -> Option<ZusatzAttribut> {
let name = name.into();
#[cfg(feature = "json")]
let wert = serde_json::Value::String(wert.into());
#[cfg(not(feature = "json"))]
let wert = wert.into();
self.put_zusatz_attribut(ZusatzAttribut {
name: Some(name),
wert: Some(wert),
..Default::default()
})
}
fn set_zusatz_attribut_in(
&mut self,
namespace: &Namespace,
key: &str,
wert: impl Into<String>,
) -> Option<ZusatzAttribut> {
self.set_zusatz_attribut(namespace.name(key), wert)
}
fn put_zusatz_attribut(&mut self, attribut: ZusatzAttribut) -> Option<ZusatzAttribut> {
let slot = self
.zusatz_attribute_field_mut()
.get_or_insert_with(Vec::new);
if let Some(name) = attribut.name.as_deref() {
if let Some(existing) = slot.iter_mut().find(|a| a.name.as_deref() == Some(name)) {
return Some(std::mem::replace(existing, attribut));
}
}
slot.push(attribut);
None
}
fn remove_zusatz_attribut(&mut self, name: &str) -> Option<ZusatzAttribut> {
let slot = self.zusatz_attribute_field_mut().as_mut()?;
let at = slot.iter().position(|a| a.name.as_deref() == Some(name))?;
Some(slot.remove(at))
}
fn remove_zusatz_attribut_in(
&mut self,
namespace: &Namespace,
key: &str,
) -> Option<ZusatzAttribut> {
self.remove_zusatz_attribut(&namespace.name(key))
}
fn remove_zusatz_attribute_in(&mut self, namespace: &Namespace) -> Vec<ZusatzAttribut> {
let Some(slot) = self.zusatz_attribute_field_mut().as_mut() else {
return Vec::new();
};
let mut taken = Vec::new();
let mut i = 0;
while i < slot.len() {
let matches = slot[i]
.name
.as_deref()
.is_some_and(|n| namespace.key_of(n).is_some());
if matches {
taken.push(slot.remove(i));
} else {
i += 1;
}
}
taken
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn zusatz_attribut_as<T: serde::de::DeserializeOwned>(
&self,
name: &str,
) -> Option<Result<T, serde_json::Error>> {
let wert = self.zusatz_attribut(name)?.wert.as_ref()?;
Some(T::deserialize(wert))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn zusatz_attribut_as_in<T: serde::de::DeserializeOwned>(
&self,
namespace: &Namespace,
key: &str,
) -> Option<Result<T, serde_json::Error>> {
self.zusatz_attribut_as(&namespace.name(key))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn set_zusatz_attribut_as<T: serde::Serialize + ?Sized>(
&mut self,
name: impl Into<String>,
value: &T,
) -> Result<Option<ZusatzAttribut>, serde_json::Error> {
let wert = serde_json::to_value(value)?;
Ok(self.put_zusatz_attribut(ZusatzAttribut {
name: Some(name.into()),
wert: Some(wert),
..Default::default()
}))
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn set_zusatz_attribut_as_in<T: serde::Serialize + ?Sized>(
&mut self,
namespace: &Namespace,
key: &str,
value: &T,
) -> Result<Option<ZusatzAttribut>, serde_json::Error> {
self.set_zusatz_attribut_as(namespace.name(key), value)
}
fn has_zusatz_attribut_key<T: ?Sized>(&self, key: &AttributKey<T>) -> bool {
self.zusatz_attribut(&key.name()).is_some()
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn zusatz_attribut_key<T: serde::de::DeserializeOwned>(
&self,
key: &AttributKey<T>,
) -> Option<Result<T, serde_json::Error>> {
self.zusatz_attribut_as(&key.name())
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
fn set_zusatz_attribut_key<T: serde::Serialize>(
&mut self,
key: &AttributKey<T>,
value: &T,
) -> Result<Option<ZusatzAttribut>, serde_json::Error> {
self.set_zusatz_attribut_as(key.name(), value)
}
fn remove_zusatz_attribut_key<T: ?Sized>(
&mut self,
key: &AttributKey<T>,
) -> Option<ZusatzAttribut> {
self.remove_zusatz_attribut(&key.name())
}
}
impl<T: HasZusatzAttribute + ?Sized> ZusatzAttributeExt for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::current::{Marktlokation, SteuerbareRessource};
#[test]
fn namespace_rejects_what_would_be_ambiguous() {
assert_eq!(Namespace::new(""), Err(NamespaceError::Empty));
assert_eq!(
Namespace::new("a:b"),
Err(NamespaceError::ContainsSeparator)
);
assert!(matches!(
Namespace::new("a b"),
Err(NamespaceError::InvalidCharacter { position: 1, .. })
));
assert!(Namespace::new("acme-billing_2").is_ok());
}
#[test]
fn registered_keys_are_well_formed() {
fn check(name: &str, ns: &Namespace, key: &str) {
assert!(
ns.is_registered(),
"{name}: {ns} is not a registered namespace"
);
assert!(!key.is_empty(), "{name}: empty key");
assert!(
!key.contains(SEPARATOR),
"{name}: key contains the separator"
);
assert_eq!(Namespace::split(name), Some((ns.as_str(), key)));
}
check(
&well_known::ZAEHLPUNKT.name(),
well_known::ZAEHLPUNKT.namespace(),
well_known::ZAEHLPUNKT.key(),
);
}
#[test]
fn registered_namespaces_are_well_formed_and_unique() {
let mut seen = std::collections::BTreeSet::new();
for ns in Namespace::REGISTERED {
assert!(Namespace::new(ns.as_str()).is_ok(), "{ns}");
assert!(ns.is_registered());
assert!(seen.insert(ns.as_str()), "duplicate namespace {ns}");
}
assert!(!Namespace::new("acme").unwrap().is_registered());
}
#[test]
fn display_and_from_str_round_trip() {
let ns = Namespace::HEMS;
assert_eq!(ns.to_string(), "hems:");
assert_eq!("hems:".parse::<Namespace>().unwrap(), ns);
assert_eq!("hems".parse::<Namespace>().unwrap(), ns);
}
#[test]
fn split_takes_the_first_separator_only() {
assert_eq!(Namespace::split("mako:ref:1"), Some(("mako", "ref:1")));
assert_eq!(Namespace::split(":orphan"), None);
assert_eq!(Namespace::HEMS.key_of("hems:a:b"), Some("a:b"));
assert_eq!(Namespace::HEMS.key_of("mako:a"), None);
assert_eq!(Namespace::HEMS.key_of("plain"), None);
}
#[test]
fn set_replaces_rather_than_appends() {
let mut sr = SteuerbareRessource::default();
assert!(sr
.set_zusatz_attribut_in(&Namespace::HEMS, "ski", "aaa")
.is_none());
let old = sr
.set_zusatz_attribut_in(&Namespace::HEMS, "ski", "bbb")
.expect("the first value comes back");
assert_eq!(old.name.as_deref(), Some("hems:ski"));
assert_eq!(sr.zusatz_attribute().count(), 1);
assert_eq!(
sr.zusatz_attribut_str_in(&Namespace::HEMS, "ski"),
Some("bbb")
);
}
#[test]
fn namespaces_do_not_collide() {
let mut malo = Marktlokation::default();
malo.set_zusatz_attribut_in(&Namespace::MAKO, "id", "M-1");
malo.set_zusatz_attribut_in(&Namespace::HEMS, "id", "H-1");
assert_eq!(malo.zusatz_attribute().count(), 2);
assert_eq!(
malo.zusatz_attribut_str_in(&Namespace::MAKO, "id"),
Some("M-1")
);
assert_eq!(
malo.zusatz_attribut_str_in(&Namespace::HEMS, "id"),
Some("H-1")
);
assert_eq!(malo.zusatz_attribut_namespaces(), ["mako", "hems"]);
malo.set_zusatz_attribut(":orphan", "x");
assert_eq!(malo.zusatz_attribut_namespaces(), ["mako", "hems"]);
}
#[test]
fn removing_a_namespace_leaves_the_others() {
let mut malo = Marktlokation::default();
malo.set_zusatz_attribut_in(&Namespace::MAKO, "a", "1");
malo.set_zusatz_attribut("kundennummer", "K-9");
malo.set_zusatz_attribut_in(&Namespace::MAKO, "b", "2");
let taken = malo.remove_zusatz_attribute_in(&Namespace::MAKO);
assert_eq!(taken.len(), 2);
assert_eq!(malo.zusatz_attribute().count(), 1);
assert_eq!(malo.zusatz_attribut_str("kundennummer"), Some("K-9"));
assert!(!malo.has_zusatz_attribute_in(&Namespace::MAKO));
}
#[test]
fn remove_on_an_absent_list_is_a_no_op() {
let mut malo = Marktlokation::default();
assert!(malo.remove_zusatz_attribut("nope").is_none());
assert!(malo.remove_zusatz_attribute_in(&Namespace::HEMS).is_empty());
assert!(malo.zusatz_attribute_field().is_none());
}
#[test]
fn nameless_attributes_are_appended() {
let mut malo = Marktlokation::default();
malo.put_zusatz_attribut(ZusatzAttribut::default());
malo.put_zusatz_attribut(ZusatzAttribut::default());
assert_eq!(malo.zusatz_attribute().count(), 2);
}
#[cfg(feature = "json")]
#[test]
fn typed_values_round_trip() {
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Steuerung {
variante: String,
stufen: u8,
}
let mut sr = SteuerbareRessource::default();
sr.set_zusatz_attribut_as_in(
&Namespace::HEMS,
"steuerung",
&Steuerung {
variante: "EMS".into(),
stufen: 4,
},
)
.unwrap();
let read: Steuerung = sr
.zusatz_attribut_as_in(&Namespace::HEMS, "steuerung")
.unwrap()
.unwrap();
assert_eq!(read.stufen, 4);
assert_eq!(
sr.zusatz_attribut_str_in(&Namespace::HEMS, "steuerung"),
None
);
assert!(sr
.zusatz_attribut_as_in::<u8>(&Namespace::HEMS, "steuerung")
.unwrap()
.is_err());
}
}