pub mod cdr;
pub mod country;
pub mod currency;
mod datetime;
mod de;
mod duration;
mod energy;
pub mod guess;
pub mod json;
pub mod lint;
mod money;
mod number;
pub mod price;
pub mod tariff;
pub mod timezone;
mod types;
mod v211;
mod v221;
pub mod warning;
use std::{collections::BTreeSet, fmt};
pub(crate) use datetime::DateTime;
use datetime::{Date, Time};
use duration::{HoursDecimal, SecondsRound};
use energy::{Ampere, Kw, Kwh};
pub(crate) use number::Number;
use serde::{Deserialize, Deserializer};
use types::DayOfWeek;
use warning::IntoCaveat;
pub use money::{Money, Price, Vat};
#[doc(inline)]
pub use warning::{Caveat, Verdict, VerdictExt, Warning};
pub type UnexpectedFields = BTreeSet<String>;
pub type TariffId = String;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Version {
V221,
V211,
}
impl Versioned for Version {
fn version(&self) -> Version {
*self
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Version::V221 => f.write_str("v221"),
Version::V211 => f.write_str("v211"),
}
}
}
pub trait Versioned: fmt::Debug {
fn version(&self) -> Version;
}
pub trait Unversioned: fmt::Debug {
type Versioned: Versioned;
fn force_into_versioned(self, version: Version) -> Self::Versioned;
}
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct OutOfRange(());
impl std::error::Error for OutOfRange {}
impl OutOfRange {
const fn new() -> OutOfRange {
OutOfRange(())
}
}
impl fmt::Display for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "out of range")
}
}
impl fmt::Debug for OutOfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "out of range")
}
}
pub struct ParseError {
object: ObjectType,
kind: ParseErrorKind,
}
pub enum ParseErrorKind {
Erased(Box<dyn std::error::Error + Send + Sync + 'static>),
Json(json::Error),
ShouldBeAnObject,
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
ParseErrorKind::Erased(err) => Some(&**err),
ParseErrorKind::Json(err) => Some(err),
ParseErrorKind::ShouldBeAnObject => None,
}
}
}
impl fmt::Debug for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "while deserializing {:?}: ", self.object)?;
match &self.kind {
ParseErrorKind::Erased(err) => write!(f, "{err}"),
ParseErrorKind::Json(err) => write!(f, "{err}"),
ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
}
}
}
impl ParseError {
fn from_cdr_err(err: json::Error) -> Self {
Self {
object: ObjectType::Tariff,
kind: ParseErrorKind::Json(err),
}
}
fn from_tariff_err(err: json::Error) -> Self {
Self {
object: ObjectType::Tariff,
kind: ParseErrorKind::Json(err),
}
}
fn from_cdr_serde_err(err: serde_json::Error) -> Self {
Self {
object: ObjectType::Cdr,
kind: ParseErrorKind::Erased(err.into()),
}
}
fn from_tariff_serde_err(err: serde_json::Error) -> Self {
Self {
object: ObjectType::Tariff,
kind: ParseErrorKind::Erased(err.into()),
}
}
fn cdr_should_be_object() -> ParseError {
Self {
object: ObjectType::Cdr,
kind: ParseErrorKind::ShouldBeAnObject,
}
}
fn tariff_should_be_object() -> ParseError {
Self {
object: ObjectType::Tariff,
kind: ParseErrorKind::ShouldBeAnObject,
}
}
pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
(self.object, self.kind)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ObjectType {
Cdr,
Tariff,
}
fn null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[cfg(test)]
mod test {
use std::{env, fmt, io::IsTerminal as _, path::Path, sync::Once};
use rust_decimal::Decimal;
use serde::{
de::{value::StrDeserializer, IntoDeserializer as _},
Deserialize,
};
use tracing::debug;
use tracing_subscriber::util::SubscriberInitExt as _;
use crate::{json, DateTime};
#[track_caller]
pub fn setup() {
static INITIALIZED: Once = Once::new();
INITIALIZED.call_once_force(|state| {
if state.is_poisoned() {
return;
}
let is_tty = std::io::stderr().is_terminal();
let level = match env::var("RUST_LOG") {
Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
Err(err) => match err {
env::VarError::NotPresent => tracing::Level::INFO,
env::VarError::NotUnicode(_) => {
panic!("`RUST_LOG` is not unicode");
}
},
};
let subscriber = tracing_subscriber::fmt()
.with_ansi(is_tty)
.with_file(true)
.with_level(false)
.with_line_number(true)
.with_max_level(level)
.with_target(false)
.with_test_writer()
.without_time()
.finish();
subscriber
.try_init()
.expect("Init tracing_subscriber::Subscriber");
});
}
pub trait AsDecimal {
fn as_dec(&self) -> &Decimal;
}
pub trait DecimalPartialEq<Rhs = Self> {
#[must_use]
fn eq_dec(&self, other: &Rhs) -> bool;
}
#[track_caller]
pub fn assert_no_unexpected_fields(unexpected_fields: &json::UnexpectedFields<'_>) {
if !unexpected_fields.is_empty() {
const MAX_FIELD_DISPLAY: usize = 20;
if unexpected_fields.len() > MAX_FIELD_DISPLAY {
let truncated_fields = unexpected_fields
.iter()
.take(MAX_FIELD_DISPLAY)
.map(|path| path.to_string())
.collect::<Vec<_>>();
panic!(
"Unexpected fields found({}); displaying the first ({}): \n{}\n... and {} more",
unexpected_fields.len(),
truncated_fields.len(),
truncated_fields.join(",\n"),
unexpected_fields.len() - truncated_fields.len(),
)
} else {
panic!(
"Unexpected fields found({}):\n{}",
unexpected_fields.len(),
unexpected_fields.to_strings().join(",\n")
)
};
}
}
#[track_caller]
pub fn assert_unexpected_fields(
unexpected_fields: &json::UnexpectedFields<'_>,
expected: &[&'static str],
) {
if unexpected_fields.len() != expected.len() {
let unexpected_fields = unexpected_fields
.into_iter()
.map(|path| path.to_string())
.collect::<Vec<_>>();
panic!(
"The unexpected fields and expected fields lists have different lengths.\n\nUnexpected fields found:\n{}",
unexpected_fields.join(",\n")
);
}
let unmatched_paths = unexpected_fields
.into_iter()
.zip(expected.iter())
.filter(|(a, b)| a != *b)
.collect::<Vec<_>>();
if !unmatched_paths.is_empty() {
let unmatched_paths = unmatched_paths
.into_iter()
.map(|(a, b)| format!("{a} != {b}"))
.collect::<Vec<_>>();
panic!(
"The unexpected fields don't match the expected fields.\n\nUnexpected fields found:\n{}",
unmatched_paths.join(",\n")
);
}
}
#[derive(Debug, Default)]
pub enum Expectation<T> {
Present(ExpectValue<T>),
#[default]
Absent,
}
#[derive(Debug)]
pub enum ExpectValue<T> {
Some(T),
Null,
}
impl<T> ExpectValue<T>
where
T: fmt::Debug,
{
pub fn into_option(self) -> Option<T> {
match self {
Self::Some(v) => Some(v),
Self::Null => None,
}
}
#[track_caller]
pub fn expect_value(self) -> T {
match self {
ExpectValue::Some(v) => v,
ExpectValue::Null => panic!("the field expects a value"),
}
}
}
impl<'de, T> Deserialize<'de> for Expectation<T>
where
T: Deserialize<'de>,
{
#[expect(clippy::unwrap_in_result, reason = "This is test util code")]
#[track_caller]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
if value.is_null() {
return Ok(Expectation::Present(ExpectValue::Null));
}
let v = T::deserialize(value).unwrap();
Ok(Expectation::Present(ExpectValue::Some(v)))
}
}
#[track_caller]
pub fn datetime_from_str(s: &str) -> DateTime {
let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
DateTime::deserialize(de).unwrap()
}
#[track_caller]
pub fn read_expect_json(json_file_path: &Path, feature: &str) -> Option<String> {
let json_dir = json_file_path
.parent()
.expect("The given file should live in a dir");
let json_file_name = json_file_path
.file_stem()
.expect("The `json_file_path` should be a file")
.to_str()
.expect("The `json_file_path` should have a valid name");
let expect_file_name = format!("output_{feature}__{json_file_name}.json");
debug!("Try to read expectation file: `{expect_file_name}`");
let s = std::fs::read_to_string(json_dir.join(&expect_file_name))
.ok()
.map(|mut s| {
json_strip_comments::strip(&mut s).ok();
s
});
debug!("Successfully Read expectation file: `{expect_file_name}`");
s
}
}
#[cfg(test)]
mod test_rust_decimal_arbitrary_precision {
use rust_decimal_macros::dec;
use crate::Number;
#[test]
fn should_serialize_decimal_with_12_fraction_digits() {
let dec = dec!(0.123456789012);
let actual = serde_json::to_string(&dec).unwrap();
assert_eq!(actual, r#""0.123456789012""#.to_owned());
}
#[test]
fn should_serialize_decimal_with_8_fraction_digits() {
let dec = dec!(37.12345678);
let actual = serde_json::to_string(&dec).unwrap();
assert_eq!(actual, r#""37.12345678""#.to_owned());
}
#[test]
fn should_serialize_0_decimal_without_fraction_digits() {
let dec = dec!(0);
let actual = serde_json::to_string(&dec).unwrap();
assert_eq!(actual, r#""0""#.to_owned());
}
#[test]
fn should_serialize_12_num_with_4_fraction_digits() {
let num: Number = dec!(0.1234).try_into().unwrap();
let actual = serde_json::to_string(&num).unwrap();
assert_eq!(actual, r#""0.1234""#.to_owned());
}
#[test]
fn should_serialize_8_num_with_4_fraction_digits() {
let num: Number = dec!(37.1234).try_into().unwrap();
let actual = serde_json::to_string(&num).unwrap();
assert_eq!(actual, r#""37.1234""#.to_owned());
}
#[test]
fn should_serialize_0_num_without_fraction_digits() {
let num: Number = dec!(0).try_into().unwrap();
let actual = serde_json::to_string(&num).unwrap();
assert_eq!(actual, r#""0""#.to_owned());
}
}