tackler_core/model/
txn_data.rs

1/*
2 * Tackler-NG 2023-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use itertools::Itertools;
7
8use crate::kernel::Predicate;
9use crate::kernel::hash::Hash;
10use crate::model::{TxnRefs, Txns, transaction};
11use crate::tackler;
12use tackler_api::filters::FilterDefinition;
13use tackler_api::metadata::items::{MetadataItem, TxnFilterDescription, TxnSetChecksum};
14use tackler_api::metadata::{Checksum, Metadata};
15
16pub struct TxnData {
17    metadata: Option<Metadata>,
18    txns: Txns,
19    hash: Option<Hash>,
20}
21
22pub struct TxnSet<'a> {
23    pub(crate) metadata: Option<Metadata>,
24    pub(crate) txns: TxnRefs<'a>,
25}
26
27impl TxnSet<'_> {
28    pub fn metadata(&self) -> Option<&Metadata> {
29        self.metadata.as_ref()
30    }
31    pub fn is_empty(&self) -> bool {
32        self.txns.is_empty()
33    }
34}
35
36impl TxnData {
37    pub fn len(&self) -> usize {
38        self.txns.len()
39    }
40    pub fn is_empty(&self) -> bool {
41        self.txns.is_empty()
42    }
43
44    pub fn from(
45        mdi_opt: Option<MetadataItem>,
46        txns: Txns,
47        hash: &Option<Hash>,
48    ) -> Result<TxnData, tackler::Error> {
49        let metadata = mdi_opt.map(Metadata::from_mdi);
50
51        let mut t = txns;
52        t.sort_by(transaction::ord_by_txn);
53
54        Ok(TxnData {
55            metadata,
56            txns: t,
57            hash: hash.clone(),
58        })
59    }
60
61    fn make_metadata(&self, txns: &TxnRefs<'_>) -> Result<Metadata, tackler::Error> {
62        let mut metadata = match &self.metadata {
63            Some(md) => Metadata::from_metadata(md),
64            None => Metadata::new(),
65        };
66
67        if let Some(hash) = &self.hash {
68            let new_tsc_mdi = MetadataItem::TxnSetChecksum(TxnSetChecksum {
69                size: txns.len(),
70                hash: calc_txn_checksum(txns, hash)?,
71            });
72
73            metadata.push(new_tsc_mdi);
74        }
75
76        Ok(metadata)
77    }
78
79    pub fn filter<'a>(&'a self, tf: &FilterDefinition) -> Result<TxnSet<'a>, tackler::Error> {
80        let refvec: TxnRefs<'_> = self.txns.iter().filter(|txn| tf.eval(txn)).collect();
81
82        let mut metadata = self.make_metadata(&refvec)?;
83        let filter_mdi = MetadataItem::TxnFilterDescription(TxnFilterDescription::from(tf.clone()));
84        metadata.push(filter_mdi);
85
86        Ok(TxnSet {
87            metadata: Some(metadata),
88            txns: refvec,
89            //hash: &self.hash,
90        })
91    }
92
93    pub fn get_all(&self) -> Result<TxnSet<'_>, tackler::Error> {
94        let txns: TxnRefs<'_> = self.txns.iter().collect();
95
96        let metadata = if self.hash.is_some() || self.metadata.is_some() {
97            Some(self.make_metadata(&txns)?)
98        } else {
99            None
100        };
101
102        Ok(TxnSet { metadata, txns })
103    }
104}
105
106fn calc_txn_checksum(txns: &TxnRefs<'_>, hasher: &Hash) -> Result<Checksum, tackler::Error> {
107    let uuids: Result<Vec<String>, tackler::Error> = txns
108        .iter()
109        .map(|txn| match txn.header.uuid {
110            Some(uuid) => Ok(uuid.to_string()),
111            None => {
112                let msg = "Txn without UUID. Txn UUID is mandatory with transaction set checksum calculation.";
113                Err(msg.into())
114            }
115        })
116        .collect();
117
118    let mut u = uuids?;
119    u.sort();
120
121    let dups: Vec<String> = u.iter().duplicates().cloned().collect();
122    if !dups.is_empty() {
123        let dups_count = dups.len();
124        let msg = if dups_count < 10 {
125            format!(
126                "Found {} duplicate txn uuids with txn set checksum.\nDuplicate ids are:\n{}",
127                dups.len(),
128                dups.join(",\n")
129            )
130        } else {
131            format!(
132                "Found {} duplicate txn uuids with txn set checksum.\nFirst ten duplicate ids are:\n{}",
133                dups.len(),
134                dups[0..10].join(",\n")
135            )
136        };
137        return Err(msg.into());
138    }
139
140    let cs = hasher.checksum(&u, "\n".as_bytes())?;
141    Ok(cs)
142}
143
144#[cfg(test)]
145mod tests {
146    use uuid::Uuid;
147
148    #[test]
149    // desc: check that uuid::to_string returns normalized lower-case UUID
150    fn uuid_as_lower_case() {
151        let uuid_ref = "e274c99e-1ebb-45e8-832d-58caf54ed95f";
152        let uuid_mixed = "E274C99E-1ebb-45e8-832d-58Caf54Ed95f";
153        let uuid_upper = "E274C99E-1EBB-45E8-832D-58CAF54ED95F";
154
155        assert_eq!(
156            Uuid::parse_str(uuid_ref).unwrap(/*:test:*/).to_string(),
157            uuid_ref
158        );
159        assert_eq!(
160            Uuid::parse_str(uuid_mixed).unwrap(/*:test:*/).to_string(),
161            uuid_ref
162        );
163        assert_eq!(
164            Uuid::parse_str(uuid_upper).unwrap(/*:test:*/).to_string(),
165            uuid_ref
166        );
167    }
168}