use core::fmt;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BtId(pub u16);
impl fmt::Display for BtId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&format!("BT-{}", self.0))
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BgId(pub u16);
impl fmt::Display for BgId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(&format!("BG-{}", self.0))
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Group {
Document,
Seller,
Buyer,
Delivery,
Payment,
DocumentAllowance,
DocumentCharge,
Totals,
VatBreakdown,
Attachment,
Line,
}
impl Group {
#[must_use]
pub const fn bg(self) -> Option<BgId> {
Some(match self {
Self::Document => return None,
Self::Seller => BgId(4),
Self::Buyer => BgId(7),
Self::Delivery => BgId(13),
Self::Payment => BgId(16),
Self::DocumentAllowance => BgId(20),
Self::DocumentCharge => BgId(21),
Self::Totals => BgId(22),
Self::VatBreakdown => BgId(23),
Self::Attachment => BgId(24),
Self::Line => BgId(25),
})
}
#[must_use]
pub const fn repeats(self) -> bool {
matches!(
self,
Self::DocumentAllowance | Self::DocumentCharge | Self::VatBreakdown | Self::Line
)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Path {
pub group: Group,
pub index: Option<usize>,
pub term: Option<BtId>,
}
impl Path {
#[must_use]
pub const fn term(term: BtId) -> Self {
Self {
group: Group::Document,
index: None,
term: Some(term),
}
}
#[must_use]
pub const fn group(group: Group) -> Self {
Self {
group,
index: None,
term: None,
}
}
#[must_use]
pub const fn group_term(group: Group, term: BtId) -> Self {
Self {
group,
index: None,
term: Some(term),
}
}
#[must_use]
pub const fn at(group: Group, index: usize) -> Self {
Self {
group,
index: Some(index),
term: None,
}
}
#[must_use]
pub const fn at_term(group: Group, index: usize, term: BtId) -> Self {
Self {
group,
index: Some(index),
term: Some(term),
}
}
#[must_use]
pub const fn document() -> Self {
Self {
group: Group::Document,
index: None,
term: None,
}
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = String::new();
if let Some(bg) = self.group.bg() {
s.push_str(&bg.to_string());
if let Some(i) = self.index {
s.push_str(&format!("[{i}]"));
}
}
if let Some(t) = self.term {
if !s.is_empty() {
s.push('/');
}
s.push_str(&t.to_string());
}
if s.is_empty() {
s.push_str("Invoice");
}
f.pad(&s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_the_language_the_standard_speaks() {
assert_eq!(Path::document().to_string(), "Invoice");
assert_eq!(Path::term(BtId(1)).to_string(), "BT-1");
assert_eq!(Path::group(Group::Totals).to_string(), "BG-22");
assert_eq!(
Path::group_term(Group::Totals, BtId(109)).to_string(),
"BG-22/BT-109"
);
assert_eq!(Path::at(Group::VatBreakdown, 0).to_string(), "BG-23[0]");
assert_eq!(
Path::at_term(Group::Line, 2, BtId(151)).to_string(),
"BG-25[2]/BT-151"
);
}
#[test]
fn repeating_groups_are_exactly_the_ones_that_need_an_index() {
for g in [
Group::DocumentAllowance,
Group::DocumentCharge,
Group::VatBreakdown,
Group::Line,
] {
assert!(g.repeats(), "{g:?}");
}
for g in [
Group::Document,
Group::Seller,
Group::Buyer,
Group::Delivery,
Group::Payment,
Group::Totals,
] {
assert!(!g.repeats(), "{g:?}");
}
}
#[test]
fn group_numbers_match_the_standard() {
assert_eq!(Group::Seller.bg(), Some(BgId(4)));
assert_eq!(Group::Buyer.bg(), Some(BgId(7)));
assert_eq!(Group::VatBreakdown.bg(), Some(BgId(23)));
assert_eq!(Group::Line.bg(), Some(BgId(25)));
assert_eq!(Group::Document.bg(), None);
}
#[test]
fn paths_sort_stably_for_diffable_reports() {
let mut v = [
Path::at_term(Group::Line, 2, BtId(151)),
Path::at_term(Group::Line, 0, BtId(151)),
Path::term(BtId(1)),
];
v.sort();
assert_eq!(v[0], Path::term(BtId(1)));
assert_eq!(v[1], Path::at_term(Group::Line, 0, BtId(151)));
}
}