#[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 = "versioned")]
macro_rules! impl_validators {
($ver:ident) => {
#[allow(missing_docs)]
pub mod $ver {
use crate::generated::$ver::*;
pub fn validate_marktlokation(v: &Marktlokation, _: &()) -> Result<(), garde::Error> {
let count = v.lokationsadresse.is_some() as usize
+ v.geoadresse.is_some() as usize
+ v.katasterinformation.is_some() as usize;
if count == 1 {
Ok(())
} else {
Err(garde::Error::new(
"exactly one address field must be set: \
lokationsadresse, geoadresse, or katasterinformation",
))
}
}
pub fn validate_messlokation(v: &Messlokation, _: &()) -> Result<(), garde::Error> {
let count = v.messadresse.is_some() as usize
+ v.geoadresse.is_some() as usize
+ v.katasterinformation.is_some() as usize;
if count == 1 {
Ok(())
} else {
Err(garde::Error::new(
"exactly one address field must be set: \
messadresse, geoadresse, or katasterinformation",
))
}
}
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(())
}
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(())
}
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)),
("vorausgezahlt", waehrung(&v.vorausgezahlt)),
("rabatt_brutto", waehrung(&v.rabatt_brutto)),
("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);
let present_count = netto.is_some() as usize
+ steuer.is_some() as usize
+ brutto.is_some() as usize;
if present_count == 2 {
return Err(garde::Error::new(
"if any two invoice totals (gesamtnetto, gesamtsteuer, \
gesamtbrutto) are set, all three must be present",
));
}
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(b), Some(z)) = (wert(&v.gesamtbrutto), wert(&v.zu_zahlen)) {
let expected = b
- wert(&v.vorausgezahlt).unwrap_or(Decimal::ZERO)
- wert(&v.rabatt_brutto).unwrap_or(Decimal::ZERO);
if expected != z {
return Err(garde::Error::new(format!(
"gesamtbrutto - vorausgezahlt - rabatt_brutto \
({expected}) must equal zu_zahlen ({z})"
)));
}
}
} 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 strictly before enddatum ({end})"
)));
}
}
Ok(())
}
pub fn validate_kostenposition_arithmetic(
v: &Kostenposition,
_: &(),
) -> Result<(), garde::Error> {
#[cfg(feature = "decimal")]
{
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 expected = (ep * m).round_dp(10);
let actual = b.round_dp(10);
if expected != actual {
return Err(garde::Error::new(format!(
"einzelpreis.wert ({ep}) * menge.wert ({m}) = {expected}, \
but betrag_kostenposition.wert is {actual}"
)));
}
}
} Ok(())
}
}
};
}
#[cfg(feature = "versioned")]
impl_validators!(v202501);
#[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()
}