tackler_core/model/
txn_data.rs1use 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 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 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 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 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 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().to_string(),
256 uuid_ref
257 );
258 assert_eq!(
259 Uuid::parse_str(uuid_mixed).unwrap().to_string(),
260 uuid_ref
261 );
262 assert_eq!(
263 Uuid::parse_str(uuid_upper).unwrap().to_string(),
264 uuid_ref
265 );
266 }
267}