#[derive(Debug, Clone)]
pub struct Validated<T>(T);
impl<T> Validated<T>
where
T: garde::Validate,
T::Context: Default,
{
pub fn new(value: T) -> Result<Self, garde::Report> {
value.validate()?;
Ok(Self(value))
}
#[inline]
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::ops::Deref for Validated<T> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
&self.0
}
}
impl<T> AsRef<T> for Validated<T> {
#[inline]
fn as_ref(&self) -> &T {
&self.0
}
}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> serde::Serialize for Validated<T> {
#[inline]
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
#[cfg(feature = "serde")]
impl<'de, T> serde::Deserialize<'de> for Validated<T>
where
T: serde::Deserialize<'de> + garde::Validate,
T::Context: Default,
{
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let value = T::deserialize(d)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "versioned")]
macro_rules! impl_validators {
($ver:ident) => {
#[allow(missing_docs)]
pub mod $ver {
use crate::generated::$ver::*;
fn conflicting_ortsangaben(present: [(&'static str, bool); 3]) -> Option<String> {
let set: Vec<&str> = present
.iter()
.filter(|(_, is_set)| *is_set)
.map(|(name, _)| *name)
.collect();
(set.len() > 1).then(|| set.join(", "))
}
pub fn validate_marktlokation(v: &Marktlokation, _: &()) -> Result<(), garde::Error> {
match conflicting_ortsangaben([
("lokationsadresse", v.lokationsadresse.is_some()),
("geoadresse", v.geoadresse.is_some()),
("katasterinformation", v.katasterinformation.is_some()),
]) {
None => Ok(()),
Some(set) => Err(garde::Error::new(format!(
"at most one Ortsangabe may be set, but {set} are — BO4E allows \
either an Adresse, a Geokoordinate, or a Katasteradresse"
))),
}
}
pub fn validate_messlokation(v: &Messlokation, _: &()) -> Result<(), garde::Error> {
match conflicting_ortsangaben([
("messadresse", v.messadresse.is_some()),
("geoadresse", v.geoadresse.is_some()),
("katasterinformation", v.katasterinformation.is_some()),
]) {
None => Ok(()),
Some(set) => Err(garde::Error::new(format!(
"at most one Ortsangabe may be set, but {set} are — BO4E allows \
either an Adresse, a Geokoordinate, or a Katasteradresse"
))),
}
}
#[cfg_attr(not(feature = "time"), allow(unused_variables))]
pub fn validate_vertrag_dates(v: &Vertrag, _: &()) -> Result<(), garde::Error> {
#[cfg(feature = "time")]
if let (Some(start), Some(end)) = (v.vertragsbeginn, v.vertragsende) {
if start >= end {
return Err(garde::Error::new(format!(
"vertragsbeginn ({start}) must be before vertragsende ({end})"
)));
}
}
Ok(())
}
#[cfg_attr(not(feature = "time"), allow(unused_variables))]
pub fn validate_bilanzierung_dates(
v: &Bilanzierung,
_: &(),
) -> Result<(), garde::Error> {
#[cfg(feature = "time")]
if let (Some(start), Some(end)) = (v.bilanzierungsbeginn, v.bilanzierungsende) {
if start > end {
return Err(garde::Error::new(format!(
"bilanzierungsbeginn ({start}) must be ≤ bilanzierungsende ({end})"
)));
}
}
Ok(())
}
#[cfg_attr(not(feature = "decimal"), allow(unused_variables))]
pub fn validate_rechnung_arithmetic(v: &Rechnung, _: &()) -> Result<(), garde::Error> {
#[cfg(feature = "decimal")]
{
use rust_decimal::Decimal;
let wert =
|b: &Option<Betrag>| -> Option<Decimal> { b.as_ref().and_then(|b| b.wert) };
let waehrung = |b: &Option<Betrag>| b.as_ref().and_then(|b| b.waehrung);
let currencies = [
("gesamtnetto", waehrung(&v.gesamtnetto)),
("gesamtsteuer", waehrung(&v.gesamtsteuer)),
("gesamtbrutto", waehrung(&v.gesamtbrutto)),
("rabatt_netto", waehrung(&v.rabatt_netto)),
("zu_zahlen", waehrung(&v.zu_zahlen)),
];
let mut first_currency = None;
let mut first_field = "";
for (field, currency) in currencies {
if let Some(c) = currency {
match first_currency {
None => {
first_currency = Some(c);
first_field = field;
}
Some(fc) if fc != c => {
return Err(garde::Error::new(format!(
"currency mismatch: {first_field} uses {fc:?} \
but {field} uses {c:?} — all Betrag fields in a \
Rechnung must use the same Waehrungscode"
)));
}
_ => {}
}
}
}
let netto = wert(&v.gesamtnetto);
let steuer = wert(&v.gesamtsteuer);
let brutto = wert(&v.gesamtbrutto);
if let (Some(n), Some(s), Some(b)) = (netto, steuer, brutto) {
if n + s != b {
return Err(garde::Error::new(format!(
"gesamtnetto ({n}) + gesamtsteuer ({s}) must equal \
gesamtbrutto ({b})"
)));
}
}
if let (Some(entries), Some(total)) = (v.steuerbetraege.as_deref(), steuer) {
let all_stated =
!entries.is_empty() && entries.iter().all(|e| e.steuerwert.is_some());
if all_stated {
let summed = entries
.iter()
.filter_map(|e| e.steuerwert)
.try_fold(Decimal::ZERO, |acc, v| acc.checked_add(v));
match summed {
Some(sum) if sum != total => {
return Err(garde::Error::new(format!(
"steuerbetraege sum to {sum}, but gesamtsteuer \
is {total}"
)));
}
None => {
return Err(garde::Error::new(
"steuerbetraege overflow the Decimal range when summed",
));
}
_ => {}
}
}
}
} Ok(())
}
pub fn validate_zeitraum(v: &Zeitraum, _: &()) -> Result<(), garde::Error> {
let has_duration = v.dauer.is_some();
let has_date = v.startdatum.is_some() || v.enddatum.is_some();
let has_time = v.startuhrzeit.is_some() || v.enduhrzeit.is_some();
if !has_duration && !has_date && !has_time {
return Err(garde::Error::new(
"Zeitraum must have at least one of: dauer, startdatum/enddatum, \
or startuhrzeit/enduhrzeit",
));
}
#[cfg(feature = "time")]
if let (Some(start), Some(end)) = (v.startdatum, v.enddatum) {
if start > end {
return Err(garde::Error::new(format!(
"startdatum ({start}) must be on or before enddatum ({end}); \
both bounds are inclusive, so a one-day period has start == end"
)));
}
}
#[cfg(feature = "time")]
if let (Some(Ok(start)), Some(Ok(end))) = (v.start_instant(), v.end_instant()) {
if start >= end {
return Err(garde::Error::new(format!(
"start instant ({start}) must be strictly before end instant \
({end}); startuhrzeit is inclusive and enduhrzeit exclusive, \
so the period would enclose no time at all"
)));
}
}
Ok(())
}
#[cfg_attr(not(feature = "decimal"), allow(unused_variables))]
pub fn validate_kostenposition_arithmetic(
v: &Kostenposition,
_: &(),
) -> Result<(), garde::Error> {
#[cfg(feature = "decimal")]
{
use rust_decimal::Decimal;
if v.zeitmenge.is_some() {
return Ok(());
}
let betrag = v.betrag_kostenposition.as_ref().and_then(|b| b.wert);
let einzelpreis = v.einzelpreis.as_ref().and_then(|p| p.wert);
let menge = v.menge.as_ref().and_then(|m| m.wert);
if let (Some(ep), Some(m), Some(b)) = (einzelpreis, menge, betrag) {
let Some(product) = ep.checked_mul(m) else {
return Err(garde::Error::new(format!(
"einzelpreis ({ep}) * menge ({m}) overflows the Decimal range"
)));
};
let scale = b.scale();
let half_ulp = scale
.checked_add(1)
.and_then(|s| Decimal::try_new(5, s).ok())
.unwrap_or(Decimal::ZERO);
if (product - b).abs() > half_ulp {
return Err(garde::Error::new(format!(
"einzelpreis.wert ({ep}) * menge.wert ({m}) = {product}, \
which does not round to betrag_kostenposition.wert ({b}) \
at its own scale of {scale} decimal place(s)"
)));
}
}
} Ok(())
}
pub mod quality {
use super::Rechnung;
#[cfg_attr(not(feature = "decimal"), allow(unused_variables))]
pub fn rechnung_totals_are_complete(v: &Rechnung) -> Result<(), garde::Error> {
#[cfg(feature = "decimal")]
{
let stated = [
("gesamtnetto", v.gesamtnetto.as_ref().and_then(|b| b.wert)),
("gesamtsteuer", v.gesamtsteuer.as_ref().and_then(|b| b.wert)),
("gesamtbrutto", v.gesamtbrutto.as_ref().and_then(|b| b.wert)),
];
let missing: Vec<&str> = stated
.iter()
.filter(|(_, amount)| amount.is_none())
.map(|(name, _)| *name)
.collect();
if missing.len() == 1 {
return Err(garde::Error::new(format!(
"two of the three invoice totals are stated, so {} is \
derivable and should be stated too",
missing[0],
)));
}
}
Ok(())
}
}
}
};
}
#[cfg(feature = "versioned")]
impl_validators!(v202607);
#[cfg(feature = "versioned")]
#[cfg_attr(docsrs, doc(cfg(feature = "versioned")))]
pub mod current {
pub use super::v202607::*;
}
#[cfg(feature = "validate")]
#[cfg_attr(docsrs, doc(cfg(feature = "validate")))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationFailure {
pub path: String,
pub message: String,
}
#[cfg(feature = "validate")]
#[cfg_attr(docsrs, doc(cfg(feature = "validate")))]
pub fn report_errors(report: &garde::Report) -> Vec<ValidationFailure> {
report
.iter()
.map(|(path, error)| ValidationFailure {
path: path.to_string(),
message: error.to_string(),
})
.collect()
}