use core::fmt;
pub mod v2_2_1_v2_3_0;
pub mod wire;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Loss {
pub pointer: String,
pub reason: String,
}
impl fmt::Display for Loss {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let at = if self.pointer.is_empty() { "/" } else { &self.pointer };
write!(f, "{at}: {}", self.reason)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Lossy(Vec<Loss>);
impl Lossy {
#[must_use]
pub fn none() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn as_slice(&self) -> &[Loss] {
&self.0
}
pub fn iter(&self) -> core::slice::Iter<'_, Loss> {
self.0.iter()
}
pub fn record(&mut self, pointer: impl Into<String>, reason: impl Into<String>) {
self.0.push(Loss { pointer: pointer.into(), reason: reason.into() });
}
pub fn absorb(&mut self, prefix: &str, other: Self) {
for loss in other.0 {
self.0.push(Loss { pointer: format!("{prefix}{}", loss.pointer), reason: loss.reason });
}
}
#[must_use]
pub fn to_status_message(&self) -> Option<String> {
if self.is_empty() {
return None;
}
Some(format!(
"version bridged with {} loss(es): {}",
self.0.len(),
self.0.iter().map(ToString::to_string).collect::<Vec<_>>().join("; ")
))
}
}
impl<'a> IntoIterator for &'a Lossy {
type Item = &'a Loss;
type IntoIter = core::slice::Iter<'a, Loss>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl IntoIterator for Lossy {
type Item = Loss;
type IntoIter = std::vec::IntoIter<Loss>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl fmt::Display for Lossy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, loss) in self.0.iter().enumerate() {
if i > 0 {
f.write_str("; ")?;
}
write!(f, "{loss}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Converted<T> {
pub value: T,
pub lossy: Lossy,
}
impl<T> Converted<T> {
#[must_use]
pub fn lossless(value: T) -> Self {
Self { value, lossy: Lossy::none() }
}
#[must_use]
pub fn new(value: T, lossy: Lossy) -> Self {
Self { value, lossy }
}
#[must_use]
pub fn is_lossless(&self) -> bool {
self.lossy.is_empty()
}
#[must_use]
pub fn ignore_losses(self) -> T {
self.value
}
pub fn into_lossless(self) -> Result<T, Lossy> {
if self.lossy.is_empty() { Ok(self.value) } else { Err(self.lossy) }
}
#[must_use]
pub fn expect_lossless(self) -> T {
assert!(self.lossy.is_empty(), "conversion was not lossless: {}", self.lossy);
self.value
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Converted<U> {
Converted { value: f(self.value), lossy: self.lossy }
}
}
impl<T> core::ops::Deref for Converted<T> {
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
pub trait Upgrade<T> {
fn upgrade(self) -> Converted<T>;
}
pub trait Downgrade<T> {
fn downgrade(self) -> Converted<T>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn losses_are_lifted_into_the_parents_coordinates() {
let mut child = Lossy::none();
child.record("/help_phone", "not present in OCPI 2.2.1");
let mut parent = Lossy::none();
parent.absorb("/evses/0", child);
assert_eq!(parent.as_slice()[0].pointer, "/evses/0/help_phone");
}
#[test]
fn a_lossless_conversion_unwraps_and_a_lossy_one_does_not() {
let clean: Converted<u8> = Converted::lossless(7);
assert!(clean.is_lossless());
assert_eq!(clean.into_lossless().unwrap(), 7);
let mut lossy = Lossy::none();
lossy.record("/x", "dropped");
let dirty = Converted::new(7u8, lossy);
assert!(dirty.clone().into_lossless().is_err());
assert_eq!(dirty.ignore_losses(), 7);
}
#[test]
fn a_report_renders_as_a_status_message() {
assert_eq!(Lossy::none().to_status_message(), None);
let mut lossy = Lossy::none();
lossy.record("/help_phone", "not present in OCPI 2.2.1");
let message = lossy.to_status_message().unwrap();
assert!(message.contains("1 loss(es)") && message.contains("/help_phone"), "{message}");
}
}