Skip to main content

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
16#[derive(Debug)]
17pub struct TxnData {
18    metadata: Option<Metadata>,
19    txns: Txns,
20    hash: Option<Hash>,
21}
22
23pub struct TxnSet<'a> {
24    pub(crate) metadata: Option<Metadata>,
25    pub(crate) txns: TxnRefs<'a>,
26}
27
28impl TxnSet<'_> {
29    #[must_use]
30    pub fn metadata(&self) -> Option<&Metadata> {
31        self.metadata.as_ref()
32    }
33    #[must_use]
34    pub fn is_empty(&self) -> bool {
35        self.txns.is_empty()
36    }
37}
38
39impl TxnData {
40    #[must_use]
41    pub fn len(&self) -> usize {
42        self.txns.len()
43    }
44
45    #[must_use]
46    pub fn is_empty(&self) -> bool {
47        self.txns.is_empty()
48    }
49
50    pub fn from(mdi_opt: Option<MetadataItem>, txns: Txns, hash: &Option<Hash>) -> TxnData {
51        let metadata = mdi_opt.map(Metadata::from_mdi);
52
53        let mut t = txns;
54        t.sort_by(transaction::ord_by_txn);
55
56        TxnData {
57            metadata,
58            txns: t,
59            hash: hash.clone(),
60        }
61    }
62
63    /// Append `TxnData` to existing `TxnData`
64    ///
65    /// This will reset the Metadata of target `TxnData`
66    ///
67    /// # Errors
68    ///
69    /// Returns `Err` in case resulting Txn Set is not valid (e.g. there are missing UUIDs)
70    /// This could happen especially when Txns with Audit information are appended with
71    /// plain Txns without UUIDs.
72    pub fn append(&mut self, txn_data: &mut TxnData) -> Result<&mut Self, tackler::Error> {
73        self.txns.append(&mut txn_data.txns);
74        let metadata =
75            TxnData::make_metadata(self.hash.as_ref(), None, &self.txns.iter().collect())?;
76        self.metadata = Some(metadata);
77        Ok(self)
78    }
79
80    fn make_metadata(
81        hash_opt: Option<&Hash>,
82        metadata_opt: Option<&Metadata>,
83        txns: &TxnRefs<'_>,
84    ) -> Result<Metadata, tackler::Error> {
85        let mut metadata = match metadata_opt {
86            Some(md) => Metadata::from_metadata(md),
87            None => Metadata::new(),
88        };
89
90        if let Some(hash) = hash_opt {
91            let new_tsc_mdi = MetadataItem::TxnSetChecksum(TxnSetChecksum {
92                size: txns.len(),
93                hash: calc_txn_checksum(txns, hash)?,
94            });
95
96            metadata.push(new_tsc_mdi);
97        }
98
99        Ok(metadata)
100    }
101
102    /// # Errors
103    /// Returns `Err` in case resulting Txn Set is not valid (e.g. there are missing UUIDs)
104    pub fn filter(&self, tf: &FilterDefinition) -> Result<TxnSet<'_>, tackler::Error> {
105        let refvec: TxnRefs<'_> = self.txns.iter().filter(|txn| tf.eval(txn)).collect();
106
107        let mut metadata =
108            TxnData::make_metadata(self.hash.as_ref(), self.metadata.as_ref(), &refvec)?;
109        let filter_mdi = MetadataItem::TxnFilterDescription(TxnFilterDescription::from(tf.clone()));
110        metadata.push(filter_mdi);
111
112        Ok(TxnSet {
113            metadata: Some(metadata),
114            txns: refvec,
115        })
116    }
117
118    /// # Errors
119    /// Returns `Err` in case resulting Txn Set is not valid (e.g. there are missing UUIDs)
120    pub fn get_all(&self) -> Result<TxnSet<'_>, tackler::Error> {
121        let txns: TxnRefs<'_> = self.txns.iter().collect();
122
123        let metadata = if self.hash.is_some() || self.metadata.is_some() {
124            Some(TxnData::make_metadata(
125                self.hash.as_ref(),
126                self.metadata.as_ref(),
127                &txns,
128            )?)
129        } else {
130            None
131        };
132
133        Ok(TxnSet { metadata, txns })
134    }
135}
136
137fn calc_txn_checksum(txns: &TxnRefs<'_>, hasher: &Hash) -> Result<Checksum, tackler::Error> {
138    let uuids: Result<Vec<String>, tackler::Error> = txns
139        .iter()
140        .map(|txn| if let Some(uuid) = txn.header.uuid { Ok(uuid.to_string()) } else {
141            let msg = "Txn without UUID. Txn UUID is mandatory with transaction set checksum calculation.";
142            Err(msg.into())
143        })
144        .collect();
145
146    let mut u = uuids?;
147    u.sort();
148
149    let dups: Vec<String> = u.iter().duplicates().cloned().collect();
150    if !dups.is_empty() {
151        let dups_count = dups.len();
152        let msg = if dups_count < 10 {
153            format!(
154                "Found {} duplicate txn uuids with txn set checksum.\nDuplicate ids are:\n{}",
155                dups.len(),
156                dups.join(",\n")
157            )
158        } else {
159            format!(
160                "Found {} duplicate txn uuids with txn set checksum.\nFirst ten duplicate ids are:\n{}",
161                dups.len(),
162                dups[0..10].join(",\n")
163            )
164        };
165        return Err(msg.into());
166    }
167
168    let cs = hasher.checksum(&u, "\n".as_bytes());
169    Ok(cs)
170}
171
172#[cfg(test)]
173mod tests {
174    use uuid::Uuid;
175
176    #[test]
177    // desc: check that uuid::to_string returns normalized lower-case UUID
178    fn uuid_as_lower_case() {
179        let uuid_ref = "e274c99e-1ebb-45e8-832d-58caf54ed95f";
180        let uuid_mixed = "E274C99E-1ebb-45e8-832d-58Caf54Ed95f";
181        let uuid_upper = "E274C99E-1EBB-45E8-832D-58CAF54ED95F";
182
183        assert_eq!(
184            Uuid::parse_str(uuid_ref).unwrap(/*:test:*/).to_string(),
185            uuid_ref
186        );
187        assert_eq!(
188            Uuid::parse_str(uuid_mixed).unwrap(/*:test:*/).to_string(),
189            uuid_ref
190        );
191        assert_eq!(
192            Uuid::parse_str(uuid_upper).unwrap(/*:test:*/).to_string(),
193            uuid_ref
194        );
195    }
196}