1pub mod cdr;
138pub mod country;
139pub mod currency;
140pub mod datetime;
141pub mod duration;
142mod energy;
143pub mod guess;
144pub mod json;
145pub mod lint;
146pub mod money;
147pub mod number;
148pub mod price;
149pub mod string;
150pub mod tariff;
151pub mod timezone;
152mod v211;
153mod v221;
154pub mod warning;
155pub mod weekday;
156
157use std::{collections::BTreeSet, fmt};
158
159#[expect(unused_imports, reason = "Soon to be used in `mod generator`")]
160use string::CiString;
161use warning::IntoCaveat;
162use weekday::Weekday;
163
164#[doc(inline)]
165pub use duration::{ToDuration, ToHoursDecimal};
166#[doc(inline)]
167pub use energy::{Ampere, Kw, Kwh};
168#[doc(inline)]
169pub use money::{Cost, Money, Price, Vat, VatApplicable};
170#[doc(inline)]
171pub use warning::{Caveat, Verdict, VerdictExt, Warning};
172
173pub type UnexpectedFields = BTreeSet<String>;
175
176pub type TariffId = String;
178
179#[derive(Clone, Copy, Debug, PartialEq)]
181pub enum Version {
182 V221,
183 V211,
184}
185
186impl Versioned for Version {
187 fn version(&self) -> Version {
188 *self
189 }
190}
191
192impl fmt::Display for Version {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Version::V221 => f.write_str("v221"),
196 Version::V211 => f.write_str("v211"),
197 }
198 }
199}
200
201pub trait Versioned: fmt::Debug {
203 fn version(&self) -> Version;
205}
206
207pub trait Unversioned: fmt::Debug {
209 type Versioned: Versioned;
211
212 fn force_into_versioned(self, version: Version) -> Self::Versioned;
219}
220
221#[derive(Clone, Copy, Hash, PartialEq, Eq)]
223pub struct OutOfRange(());
224
225impl std::error::Error for OutOfRange {}
226
227impl OutOfRange {
228 const fn new() -> OutOfRange {
229 OutOfRange(())
230 }
231}
232
233impl fmt::Display for OutOfRange {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(f, "out of range")
236 }
237}
238
239impl fmt::Debug for OutOfRange {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 write!(f, "out of range")
242 }
243}
244
245pub struct ParseError {
247 object: ObjectType,
249
250 kind: ParseErrorKind,
252}
253
254pub enum ParseErrorKind {
256 Erased(Box<dyn std::error::Error + Send + Sync + 'static>),
258
259 Json(json::Error),
261
262 ShouldBeAnObject,
264}
265
266impl std::error::Error for ParseError {
267 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
268 match &self.kind {
269 ParseErrorKind::Erased(err) => Some(&**err),
270 ParseErrorKind::Json(err) => Some(err),
271 ParseErrorKind::ShouldBeAnObject => None,
272 }
273 }
274}
275
276impl fmt::Debug for ParseError {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 fmt::Display::fmt(self, f)
279 }
280}
281
282impl fmt::Display for ParseError {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 write!(f, "while deserializing {:?}: ", self.object)?;
285
286 match &self.kind {
287 ParseErrorKind::Erased(err) => write!(f, "{err}"),
288 ParseErrorKind::Json(err) => write!(f, "{err}"),
289 ParseErrorKind::ShouldBeAnObject => f.write_str("The root element should be an object"),
290 }
291 }
292}
293
294impl ParseError {
295 fn from_cdr_err(err: json::Error) -> Self {
297 Self {
298 object: ObjectType::Tariff,
299 kind: ParseErrorKind::Json(err),
300 }
301 }
302
303 fn from_tariff_err(err: json::Error) -> Self {
305 Self {
306 object: ObjectType::Tariff,
307 kind: ParseErrorKind::Json(err),
308 }
309 }
310
311 fn from_cdr_serde_err(err: serde_json::Error) -> Self {
313 Self {
314 object: ObjectType::Cdr,
315 kind: ParseErrorKind::Erased(err.into()),
316 }
317 }
318
319 fn from_tariff_serde_err(err: serde_json::Error) -> Self {
321 Self {
322 object: ObjectType::Tariff,
323 kind: ParseErrorKind::Erased(err.into()),
324 }
325 }
326
327 fn cdr_should_be_object() -> ParseError {
328 Self {
329 object: ObjectType::Cdr,
330 kind: ParseErrorKind::ShouldBeAnObject,
331 }
332 }
333
334 fn tariff_should_be_object() -> ParseError {
335 Self {
336 object: ObjectType::Tariff,
337 kind: ParseErrorKind::ShouldBeAnObject,
338 }
339 }
340
341 pub fn into_parts(self) -> (ObjectType, ParseErrorKind) {
343 (self.object, self.kind)
344 }
345}
346
347#[derive(Copy, Clone, Debug, Eq, PartialEq)]
349pub enum ObjectType {
350 Cdr,
354
355 Tariff,
359}
360
361pub(crate) trait SaturatingAdd {
365 #[must_use]
367 fn saturating_add(self, other: Self) -> Self;
368}
369
370pub(crate) trait SaturatingSub {
374 #[must_use]
376 fn saturating_sub(self, other: Self) -> Self;
377}
378
379pub(crate) struct DisplayOption<T>(Option<T>)
381where
382 T: fmt::Display;
383
384impl<T> fmt::Display for DisplayOption<T>
385where
386 T: fmt::Display,
387{
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 match &self.0 {
390 Some(v) => fmt::Display::fmt(v, f),
391 None => f.write_str("∅"),
392 }
393 }
394}
395
396#[cfg(test)]
397mod test {
398 #![allow(
399 clippy::unwrap_in_result,
400 reason = "unwraps are allowed anywhere in tests"
401 )]
402
403 use std::{env, fmt, io::IsTerminal as _, path::Path, sync::Once};
404
405 use chrono::{DateTime, Utc};
406 use rust_decimal::Decimal;
407 use serde::{
408 de::{value::StrDeserializer, IntoDeserializer as _},
409 Deserialize,
410 };
411 use tracing::debug;
412 use tracing_subscriber::util::SubscriberInitExt as _;
413
414 use crate::{datetime, json, number};
415
416 #[track_caller]
418 pub fn setup() {
419 static INITIALIZED: Once = Once::new();
420
421 INITIALIZED.call_once_force(|state| {
422 if state.is_poisoned() {
423 return;
424 }
425
426 let is_tty = std::io::stderr().is_terminal();
427
428 let level = match env::var("RUST_LOG") {
429 Ok(s) => s.parse().unwrap_or(tracing::Level::INFO),
430 Err(err) => match err {
431 env::VarError::NotPresent => tracing::Level::INFO,
432 env::VarError::NotUnicode(_) => {
433 panic!("`RUST_LOG` is not unicode");
434 }
435 },
436 };
437
438 let subscriber = tracing_subscriber::fmt()
439 .with_ansi(is_tty)
440 .with_file(true)
441 .with_level(false)
442 .with_line_number(true)
443 .with_max_level(level)
444 .with_target(false)
445 .with_test_writer()
446 .without_time()
447 .finish();
448
449 subscriber
450 .try_init()
451 .expect("Init tracing_subscriber::Subscriber");
452 });
453 }
454
455 pub trait ApproxEq<Rhs = Self> {
465 #[must_use]
466 fn approx_eq(&self, other: &Rhs) -> bool;
467 }
468
469 impl<T> ApproxEq for Option<T>
470 where
471 T: ApproxEq,
472 {
473 fn approx_eq(&self, other: &Self) -> bool {
474 match (self, other) {
475 (Some(a), Some(b)) => a.approx_eq(b),
476 (None, None) => true,
477 _ => false,
478 }
479 }
480 }
481
482 pub fn approx_eq_dec(a: Decimal, mut b: Decimal, tolerance: Decimal, precision: u32) -> bool {
484 let a = a.round_dp(precision);
485 b.rescale(number::SCALE);
486 let b = b.round_dp(precision);
487 (a - b).abs() <= tolerance
488 }
489
490 #[track_caller]
491 pub fn assert_no_unexpected_fields(unexpected_fields: &json::UnexpectedFields<'_>) {
492 if !unexpected_fields.is_empty() {
493 const MAX_FIELD_DISPLAY: usize = 20;
494
495 if unexpected_fields.len() > MAX_FIELD_DISPLAY {
496 let truncated_fields = unexpected_fields
497 .iter()
498 .take(MAX_FIELD_DISPLAY)
499 .map(|path| path.to_string())
500 .collect::<Vec<_>>();
501
502 panic!(
503 "Unexpected fields found({}); displaying the first ({}): \n{}\n... and {} more",
504 unexpected_fields.len(),
505 truncated_fields.len(),
506 truncated_fields.join(",\n"),
507 unexpected_fields.len() - truncated_fields.len(),
508 )
509 } else {
510 panic!(
511 "Unexpected fields found({}):\n{}",
512 unexpected_fields.len(),
513 unexpected_fields.to_strings().join(",\n")
514 )
515 };
516 }
517 }
518
519 #[track_caller]
520 pub fn assert_unexpected_fields(
521 unexpected_fields: &json::UnexpectedFields<'_>,
522 expected: &[&'static str],
523 ) {
524 if unexpected_fields.len() != expected.len() {
525 let unexpected_fields = unexpected_fields
526 .into_iter()
527 .map(|path| path.to_string())
528 .collect::<Vec<_>>();
529
530 panic!(
531 "The unexpected fields and expected fields lists have different lengths.\n\nUnexpected fields found:\n{}",
532 unexpected_fields.join(",\n")
533 );
534 }
535
536 let unmatched_paths = unexpected_fields
537 .into_iter()
538 .zip(expected.iter())
539 .filter(|(a, b)| a != *b)
540 .collect::<Vec<_>>();
541
542 if !unmatched_paths.is_empty() {
543 let unmatched_paths = unmatched_paths
544 .into_iter()
545 .map(|(a, b)| format!("{a} != {b}"))
546 .collect::<Vec<_>>();
547
548 panic!(
549 "The unexpected fields don't match the expected fields.\n\nUnexpected fields found:\n{}",
550 unmatched_paths.join(",\n")
551 );
552 }
553 }
554
555 #[derive(Debug, Default)]
559 pub enum Expectation<T> {
560 Present(ExpectValue<T>),
562
563 #[default]
565 Absent,
566 }
567
568 #[derive(Debug)]
570 pub enum ExpectValue<T> {
571 Some(T),
573
574 Null,
576 }
577
578 impl<T> ExpectValue<T>
579 where
580 T: fmt::Debug,
581 {
582 pub fn into_option(self) -> Option<T> {
584 match self {
585 Self::Some(v) => Some(v),
586 Self::Null => None,
587 }
588 }
589
590 #[track_caller]
596 pub fn expect_value(self) -> T {
597 match self {
598 ExpectValue::Some(v) => v,
599 ExpectValue::Null => panic!("the field expects a value"),
600 }
601 }
602 }
603
604 impl<'de, T> Deserialize<'de> for Expectation<T>
605 where
606 T: Deserialize<'de>,
607 {
608 #[expect(clippy::unwrap_in_result, reason = "This is test util code")]
609 #[track_caller]
610 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
611 where
612 D: serde::Deserializer<'de>,
613 {
614 let value = serde_json::Value::deserialize(deserializer)?;
615
616 if value.is_null() {
617 return Ok(Expectation::Present(ExpectValue::Null));
618 }
619
620 let v = T::deserialize(value).unwrap();
621 Ok(Expectation::Present(ExpectValue::Some(v)))
622 }
623 }
624
625 #[track_caller]
627 pub fn datetime_from_str(s: &str) -> DateTime<Utc> {
628 let de: StrDeserializer<'_, serde::de::value::Error> = s.into_deserializer();
629 datetime::deser_to_utc(de).unwrap()
630 }
631
632 #[track_caller]
637 pub fn read_expect_json(json_file_path: &Path, feature: &str) -> Option<String> {
638 let json_dir = json_file_path
639 .parent()
640 .expect("The given file should live in a dir");
641
642 let json_file_name = json_file_path
643 .file_stem()
644 .expect("The `json_file_path` should be a file")
645 .to_str()
646 .expect("The `json_file_path` should have a valid name");
647
648 let expect_file_name = format!("output_{feature}__{json_file_name}.json");
651
652 debug!("Try to read expectation file: `{expect_file_name}`");
653
654 let s = std::fs::read_to_string(json_dir.join(&expect_file_name))
655 .ok()
656 .map(|mut s| {
657 json_strip_comments::strip(&mut s).ok();
658 s
659 });
660
661 debug!("Successfully Read expectation file: `{expect_file_name}`");
662 s
663 }
664
665 #[track_caller]
666 pub fn assert_approx_eq_failed(
667 left: &dyn fmt::Debug,
668 right: &dyn fmt::Debug,
669 args: Option<fmt::Arguments<'_>>,
670 ) -> ! {
671 match args {
672 Some(args) => panic!(
673 "assertion `left == right` failed: {args}
674left: {left:?}
675right: {right:?}"
676 ),
677 None => panic!(
678 "assertion `left == right` failed
679left: {left:?}
680right: {right:?}"
681 ),
682 }
683 }
684
685 #[macro_export]
687 macro_rules! assert_approx_eq {
688 ($left:expr, $right:expr $(,)?) => ({
689 use $crate::test::ApproxEq;
690
691 match (&$left, &$right) {
692 (left_val, right_val) => {
693 if !((*left_val).approx_eq(&*right_val)) {
694 $crate::test::assert_approx_eq_failed(
698 &*left_val,
699 &*right_val,
700 std::option::Option::None
701 );
702 }
703 }
704 }
705 });
706 ($left:expr, $right:expr, $($arg:tt)+) => ({
707 use $crate::test::ApproxEq;
708
709 match (&$left, &$right) {
710 (left_val, right_val) => {
711 if !((*left_val).approx_eq(&*right_val)) {
712 $crate::test::assert_approx_eq_failed(
716 &*left_val,
717 &*right_val,
718 std::option::Option::Some(std::format_args!($($arg)+))
719 );
720 }
721 }
722 }
723 });
724 }
725}
726
727#[cfg(test)]
728mod test_rust_decimal_arbitrary_precision {
729 use rust_decimal_macros::dec;
730
731 #[test]
732 fn should_serialize_decimal_with_12_fraction_digits() {
733 let dec = dec!(0.123456789012);
734 let actual = serde_json::to_string(&dec).unwrap();
735 assert_eq!(actual, r#""0.123456789012""#.to_owned());
736 }
737
738 #[test]
739 fn should_serialize_decimal_with_8_fraction_digits() {
740 let dec = dec!(37.12345678);
741 let actual = serde_json::to_string(&dec).unwrap();
742 assert_eq!(actual, r#""37.12345678""#.to_owned());
743 }
744
745 #[test]
746 fn should_serialize_0_decimal_without_fraction_digits() {
747 let dec = dec!(0);
748 let actual = serde_json::to_string(&dec).unwrap();
749 assert_eq!(actual, r#""0""#.to_owned());
750 }
751
752 #[test]
753 fn should_serialize_12_num_with_4_fraction_digits() {
754 let num = dec!(0.1234);
755 let actual = serde_json::to_string(&num).unwrap();
756 assert_eq!(actual, r#""0.1234""#.to_owned());
757 }
758
759 #[test]
760 fn should_serialize_8_num_with_4_fraction_digits() {
761 let num = dec!(37.1234);
762 let actual = serde_json::to_string(&num).unwrap();
763 assert_eq!(actual, r#""37.1234""#.to_owned());
764 }
765
766 #[test]
767 fn should_serialize_0_num_without_fraction_digits() {
768 let num = dec!(0);
769 let actual = serde_json::to_string(&num).unwrap();
770 assert_eq!(actual, r#""0""#.to_owned());
771 }
772}