iati_types/
lib.rs

1//! iati-types: strongly-typed, IO-free core models for IATI Activity v2.03.
2//!
3//! Other downstream crates (e.g. 'iati-xml', 'iati-transform') can provide parsing, serialization,
4//! validation, and codelist lookups.
5
6pub mod money;
7pub mod tx;
8
9pub use money::{CurrencyCode, Money};
10pub use tx::{Transaction, TxType};
11
12use chrono::NaiveDate;
13use serde::{Deserialize, Serialize};
14
15/// Lightweight organisation reference used here through the Activity tree.
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct OrgRef {
19    /// This is the IATI identifier of the organisation or registry-specific id.
20    pub ref_id: Option<String>,
21    /// Display name (narrative text, typically first/default language).
22    pub name: Option<String>,
23}
24
25/// IATI Activity (trimmed to basic fields for foundational fields of the Activity struct here).
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27#[derive(Debug, Clone, PartialEq)]
28pub struct Activity {
29    /// 'iati-identifier' element: the unique identifier for the Activity.
30    pub iati_identifier: String,
31    /// Default ISO 4217 currency for monety values in this Activity.
32    /// Use when 'value/@currency' is not present.
33    pub default_currency: Option<CurrencyCode>,
34    /// Transactions recorded for this activity.
35    pub transactions: Vec<Transaction>,
36    /// Reporting organisation publishing this Activity.
37    pub reporting_org: Option<OrgRef>,
38    /// Activity start/end dates from 'activity-date' element.
39    pub activity_start: Option<NaiveDate>,
40    pub activity_end: Option<NaiveDate>,
41}
42
43impl Activity {
44    pub fn new<S: Into<String>>(iati_identifier: S) -> Self {
45        Self {
46            iati_identifier: iati_identifier.into(),
47            default_currency: None,
48            transactions: Vec::new(),
49            reporting_org: None,
50            activity_start: None,
51            activity_end: None,
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::tx::{Transaction, TxType};
60    use rust_decimal::Decimal;
61
62    #[test]
63    fn tx_type_roundtrip() {
64        assert_eq!(TxType::from(1).code(), 1);
65        assert_eq!(TxType::from(13).code(), 13);
66        assert!(matches!(TxType::from(99), TxType::Unknown(99)));
67        assert_eq!("3".parse::<TxType>().unwrap().code(), 3);
68    }
69
70    #[test]
71    fn money_uppercases_currency() {
72        use crate::money::{CurrencyCode, Money};
73        let m = Money {
74            amount: Decimal::new(1000, 2),
75            currency: Some(CurrencyCode::from("usd")),
76            value_date: None,
77        };
78        assert_eq!(m.currency.unwrap().0, "USD");
79    }
80
81    #[test]
82    fn transaction_new_and_builders() {
83        use crate::money::{CurrencyCode, Money};
84        use chrono::NaiveDate;
85
86        let date = NaiveDate::from_ymd_opt(2023, 5, 1).unwrap();
87        let money = Money::new(Decimal::new(5000, 2));
88
89        let tx = Transaction::new(TxType::Disbursement, date, money.clone())
90            .with_provider(OrgRef {
91                ref_id: Some("AAA-111".into()),
92                name: Some("Donor Org".into()),
93            })
94            .with_receiver(OrgRef {
95                ref_id: Some("BBB-222".into()),
96                name: None,
97            })
98            .with_currency_hint(CurrencyCode::from("EUR"));
99
100        assert_eq!(tx.tx_type, TxType::Disbursement);
101        assert_eq!(tx.date, date);
102        assert_eq!(tx.value.amount, money.amount);
103        assert_eq!(
104            tx.provider_org.as_ref().unwrap().ref_id.as_deref(),
105            Some("AAA-111")
106        );
107        assert_eq!(
108            tx.receiver_org.as_ref().unwrap().ref_id.as_deref(),
109            Some("BBB-222")
110        );
111        assert_eq!(tx.currency_hint.as_ref().unwrap().0, "EUR");
112    }
113}