Skip to main content

tackler_core/model/
txn_data.rs

1/*
2 * Tackler-NG 2023-2026
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::kernel::hash::Hash;
7use crate::kernel::{Predicate, Settings};
8use crate::model::{TxnRefs, Txns, transaction};
9use crate::tackler;
10use itertools::Itertools;
11use tackler_api::filters::FilterDefinition;
12use tackler_api::metadata::items::{MetadataItem, TxnFilterDescription, TxnSetChecksum};
13use tackler_api::metadata::{Checksum, Metadata};
14use uuid::Uuid;
15
16#[derive(Debug)]
17pub struct TxnData {
18    metadata: Option<Metadata>,
19    txns: Txns,
20    hash: Option<Hash>,
21    unique_extid: bool,
22}
23
24pub struct TxnSet<'a> {
25    pub(crate) metadata: Option<Metadata>,
26    pub(crate) txns: TxnRefs<'a>,
27}
28
29impl TxnSet<'_> {
30    #[must_use]
31    pub fn metadata(&self) -> Option<&Metadata> {
32        self.metadata.as_ref()
33    }
34    #[must_use]
35    pub fn is_empty(&self) -> bool {
36        self.txns.is_empty()
37    }
38}
39
40impl TxnData {
41    #[must_use]
42    pub fn len(&self) -> usize {
43        self.txns.len()
44    }
45
46    #[must_use]
47    pub fn is_empty(&self) -> bool {
48        self.txns.is_empty()
49    }
50
51    /// Try to create `TxnData` from set of transactions
52    ///
53    /// # Errors
54    /// If resulting txn set is logically invalid, the method will return error
55    pub fn try_from(
56        mdi_opt: Option<MetadataItem>,
57        txns: Txns,
58        settings: &Settings,
59    ) -> Result<TxnData, tackler::Error> {
60        let metadata = mdi_opt.map(Metadata::from_mdi);
61
62        if settings.audit_mode {
63            check_uuids(&txns)?;
64        }
65        if settings.is_extid_unique() {
66            check_extid(&txns)?;
67        }
68
69        let mut t = txns;
70        t.sort_by(transaction::ord_by_txn);
71
72        Ok(TxnData {
73            metadata,
74            txns: t,
75            hash: settings.get_hash().clone(),
76            unique_extid: settings.is_extid_unique(),
77        })
78    }
79
80    /// Append `TxnData` to existing `TxnData`
81    ///
82    /// This will reset the Metadata of target `TxnData`
83    ///
84    /// # Errors
85    /// Returns `Err` in case resulting txn set is invalid (e.g. there are missing UUIDs)
86    /// This could happen especially when Txns with Audit information are appended with
87    /// plain Txns without UUIDs.
88    pub fn append(&mut self, txn_data: &mut TxnData) -> Result<&mut Self, tackler::Error> {
89        self.txns.append(&mut txn_data.txns);
90
91        if self.hash.is_some() {
92            check_uuids(&self.txns)?;
93        }
94        if self.unique_extid {
95            check_extid(&self.txns)?;
96        }
97
98        let metadata =
99            TxnData::make_metadata(self.hash.as_ref(), None, &self.txns.iter().collect())?;
100        self.metadata = Some(metadata);
101        Ok(self)
102    }
103
104    fn make_metadata(
105        hash_opt: Option<&Hash>,
106        metadata_opt: Option<&Metadata>,
107        txns: &TxnRefs<'_>,
108    ) -> Result<Metadata, tackler::Error> {
109        let mut metadata = match metadata_opt {
110            Some(md) => Metadata::from_metadata(md),
111            None => Metadata::new(),
112        };
113
114        if let Some(hash) = hash_opt {
115            let new_tsc_mdi = MetadataItem::TxnSetChecksum(TxnSetChecksum {
116                size: txns.len(),
117                hash: calc_txn_checksum(txns, hash)?,
118            });
119
120            metadata.push(new_tsc_mdi);
121        }
122
123        Ok(metadata)
124    }
125
126    /// # Errors
127    /// Returns `Err` in case resulting Txn Set is not valid (e.g. there are missing UUIDs)
128    pub fn filter(&self, tf: &FilterDefinition) -> Result<TxnSet<'_>, tackler::Error> {
129        let refvec: TxnRefs<'_> = self.txns.iter().filter(|txn| tf.eval(txn)).collect();
130
131        let mut metadata =
132            TxnData::make_metadata(self.hash.as_ref(), self.metadata.as_ref(), &refvec)?;
133        let filter_mdi = MetadataItem::TxnFilterDescription(TxnFilterDescription::from(tf.clone()));
134        metadata.push(filter_mdi);
135
136        Ok(TxnSet {
137            metadata: Some(metadata),
138            txns: refvec,
139        })
140    }
141
142    /// # Errors
143    /// Returns `Err` in case resulting Txn Set is not valid (e.g. there are missing UUIDs)
144    pub fn get_all(&self) -> Result<TxnSet<'_>, tackler::Error> {
145        let txns: TxnRefs<'_> = self.txns.iter().collect();
146
147        let metadata = if self.hash.is_some() || self.metadata.is_some() {
148            Some(TxnData::make_metadata(
149                self.hash.as_ref(),
150                self.metadata.as_ref(),
151                &txns,
152            )?)
153        } else {
154            None
155        };
156
157        Ok(TxnSet { metadata, txns })
158    }
159}
160
161fn check_extid(txns: &Txns) -> Result<(), tackler::Error> {
162    let dups: Vec<&String> = txns
163        .iter()
164        .filter_map(|txn| txn.header.extid.as_ref())
165        .duplicates()
166        .collect();
167
168    if dups.is_empty() {
169        Ok(())
170    } else {
171        let dups_count = dups.len();
172        let msg = if dups_count < 10 {
173            format!(
174                "Found {} duplicate external ids.\nDuplicate ext-ids are:\n{}",
175                dups.len(),
176                dups.iter().join(",\n")
177            )
178        } else {
179            format!(
180                "Found {} duplicate external is.\nFirst ten duplicate ext-ids are:\n{}",
181                dups.len(),
182                dups[0..10].iter().join(",\n")
183            )
184        };
185        Err(msg.into())
186    }
187}
188
189fn check_uuids(txns: &Txns) -> Result<(), tackler::Error> {
190    if txns.iter().any(|txn| txn.header.uuid.is_none()) {
191        let msg =
192            "Txn without UUID. Txn UUID is mandatory with transaction set checksum calculation.";
193        return Err(msg.into());
194    }
195
196    let dups: Vec<&Uuid> = txns
197        .iter()
198        .filter_map(|txn| txn.header.uuid.as_ref())
199        .duplicates()
200        .collect();
201
202    if dups.is_empty() {
203        Ok(())
204    } else {
205        let dups_count = dups.len();
206        let msg = if dups_count < 10 {
207            format!(
208                "Found {} duplicate txn uuids with txn set checksum.\nDuplicate ids are:\n{}",
209                dups.len(),
210                dups.iter().map(|u| { u.to_string() }).join(",\n")
211            )
212        } else {
213            format!(
214                "Found {} duplicate txn uuids with txn set checksum.\nFirst ten duplicate ids are:\n{}",
215                dups.len(),
216                dups[0..10].iter().map(|u| { u.to_string() }).join(",\n")
217            )
218        };
219        Err(msg.into())
220    }
221}
222
223fn calc_txn_checksum(txns: &TxnRefs<'_>, hasher: &Hash) -> Result<Checksum, tackler::Error> {
224    let u: Result<Vec<String>, tackler::Error> = txns
225        .iter()
226        .map(|txn| {
227            if let Some(u) = txn.header.uuid {
228                Ok(u.to_string())
229            } else {
230                let msg = "Internal error: calc_txn_checksum with txns missing UUID";
231                Err(msg.into())
232            }
233        })
234        .collect();
235    let mut uuids = u?;
236
237    uuids.sort();
238
239    let cs = hasher.checksum(&uuids, "\n".as_bytes());
240    Ok(cs)
241}
242
243#[cfg(test)]
244mod tests {
245    use uuid::Uuid;
246
247    #[test]
248    // desc: check that uuid::to_string returns normalized lower-case UUID
249    fn uuid_as_lower_case() {
250        let uuid_ref = "e274c99e-1ebb-45e8-832d-58caf54ed95f";
251        let uuid_mixed = "E274C99E-1ebb-45e8-832d-58Caf54Ed95f";
252        let uuid_upper = "E274C99E-1EBB-45E8-832D-58CAF54ED95F";
253
254        assert_eq!(
255            Uuid::parse_str(uuid_ref).unwrap(/*:test:*/).to_string(),
256            uuid_ref
257        );
258        assert_eq!(
259            Uuid::parse_str(uuid_mixed).unwrap(/*:test:*/).to_string(),
260            uuid_ref
261        );
262        assert_eq!(
263            Uuid::parse_str(uuid_upper).unwrap(/*:test:*/).to_string(),
264            uuid_ref
265        );
266    }
267}