1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::fmt::Write as _;
4use std::fs;
5use std::fs::File;
6use std::path::Path;
7
8use serde_json;
9
10pub type ArtifactId = String;
11pub type FileContent = String;
12
13#[derive(Debug, Default)]
16pub struct FakeAggregatorData {
17 status: FileContent,
18
19 epoch_settings: FileContent,
20
21 certificates_list: FileContent,
22 individual_certificates: BTreeMap<ArtifactId, FileContent>,
23
24 snapshots_list: FileContent,
25 individual_snapshots: BTreeMap<ArtifactId, FileContent>,
26
27 mithril_stake_distributions_list: FileContent,
28 individual_mithril_stake_distributions: BTreeMap<ArtifactId, FileContent>,
29
30 cardano_transaction_snapshots_list: FileContent,
31 individual_cardano_transaction_snapshots: BTreeMap<ArtifactId, FileContent>,
32 cardano_transaction_proofs: BTreeMap<ArtifactId, FileContent>,
33
34 cardano_stake_distributions_list: FileContent,
35 individual_cardano_stake_distributions: BTreeMap<ArtifactId, FileContent>,
36
37 cardano_database_snapshots_list: FileContent,
38 individual_cardano_database_snapshots: BTreeMap<ArtifactId, FileContent>,
39}
40
41impl FakeAggregatorData {
42 pub fn load_from_folder(folder: &Path) -> Self {
43 let mut data = FakeAggregatorData::default();
44
45 for entry in list_json_files_in_folder(folder) {
46 let filename = entry.file_name().to_string_lossy().to_string();
47 let file_content = fs::read_to_string(entry.path()).unwrap_or_else(|_| {
48 panic!(
49 "Could not read file content, file_path: {}",
50 entry.path().display()
51 )
52 });
53
54 match filename.as_str() {
55 "status.json" => {
56 data.status = file_content;
57 }
58 "epoch-settings.json" => {
59 data.epoch_settings = file_content;
60 }
61 "mithril-stake-distributions-list.json" => {
62 data.mithril_stake_distributions_list = file_content;
63 }
64 "snapshots-list.json" => {
65 data.snapshots_list = file_content;
66 }
67 "cardano-stake-distributions-list.json" => {
68 data.cardano_stake_distributions_list = file_content;
69 }
70 "cardano-databases-list.json" => {
71 data.cardano_database_snapshots_list = file_content;
72 }
73 "certificates-list.json" => {
74 data.certificates_list = file_content;
75 }
76 "ctx-snapshots-list.json" => {
77 data.cardano_transaction_snapshots_list = file_content;
78 }
79 "mithril-stake-distributions.json" => {
80 data.individual_mithril_stake_distributions =
81 Self::read_artifacts_json_file(&entry.path());
82 }
83 "snapshots.json" => {
84 data.individual_snapshots = Self::read_artifacts_json_file(&entry.path());
85 }
86 "cardano-stake-distributions.json" => {
87 data.individual_cardano_stake_distributions =
88 Self::read_artifacts_json_file(&entry.path());
89 }
90 "cardano-databases.json" => {
91 data.individual_cardano_database_snapshots =
92 Self::read_artifacts_json_file(&entry.path());
93 }
94 "certificates.json" => {
95 data.individual_certificates = Self::read_artifacts_json_file(&entry.path());
96 }
97 "ctx-snapshots.json" => {
98 data.individual_cardano_transaction_snapshots =
99 Self::read_artifacts_json_file(&entry.path());
100 }
101 "ctx-proofs.json" => {
102 data.cardano_transaction_proofs = Self::read_artifacts_json_file(&entry.path());
103 }
104 _ => {}
106 }
107 }
108
109 data
110 }
111
112 pub fn generate_code_for_ids(self) -> String {
113 let cardano_stake_distributions_per_epoch =
114 extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch");
115 let cardano_database_snapshots_per_epoch =
116 extract_item_list_per_epoch(&self.cardano_database_snapshots_list, "/beacon/epoch");
117
118 Self::assemble_code(
119 &[
120 generate_ids_array(
121 "snapshot_digests",
122 BTreeSet::from_iter(self.individual_snapshots.keys().cloned()),
123 ),
124 generate_ids_array(
125 "mithril_stake_distribution_hashes",
126 BTreeSet::from_iter(
127 self.individual_mithril_stake_distributions.keys().cloned(),
128 ),
129 ),
130 generate_ids_array(
131 "cardano_stake_distribution_hashes",
132 BTreeSet::from_iter(
133 self.individual_cardano_stake_distributions.keys().cloned(),
134 ),
135 ),
136 generate_epoch_array(
137 "cardano_stake_distribution_epochs",
138 BTreeSet::from_iter(cardano_stake_distributions_per_epoch.keys().cloned()),
139 ),
140 generate_ids_array(
141 "cardano_database_snapshot_hashes",
142 BTreeSet::from_iter(self.individual_cardano_database_snapshots.keys().cloned()),
143 ),
144 generate_epoch_array(
145 "cardano_database_snapshot_epochs",
146 BTreeSet::from_iter(cardano_database_snapshots_per_epoch.keys().cloned()),
147 ),
148 generate_ids_array(
149 "certificate_hashes",
150 BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
151 ),
152 generate_ids_array(
153 "cardano_transaction_snapshot_hashes",
154 BTreeSet::from_iter(
155 self.individual_cardano_transaction_snapshots.keys().cloned(),
156 ),
157 ),
158 generate_ids_array(
159 "proof_transaction_hashes",
160 BTreeSet::from_iter(self.cardano_transaction_proofs.keys().cloned()),
161 ),
162 ],
163 false,
164 )
165 }
166
167 pub fn generate_code_for_all_data(self) -> String {
168 let cardano_stake_distributions_per_epoch =
169 extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch");
170 let cardano_database_snapshots_per_epoch =
171 extract_item_list_per_epoch(&self.cardano_database_snapshots_list, "/beacon/epoch");
172
173 Self::assemble_code(
174 &[
175 generate_list_getter("status", self.status),
176 generate_list_getter("epoch_settings", self.epoch_settings),
177 generate_ids_array(
178 "snapshot_digests",
179 BTreeSet::from_iter(self.individual_snapshots.keys().cloned()),
180 ),
181 generate_artifact_getter("snapshots", self.individual_snapshots),
182 generate_list_getter("snapshot_list", self.snapshots_list),
183 generate_ids_array(
184 "mithril_stake_distribution_hashes",
185 BTreeSet::from_iter(
186 self.individual_mithril_stake_distributions.keys().cloned(),
187 ),
188 ),
189 generate_artifact_getter(
190 "mithril_stake_distributions",
191 self.individual_mithril_stake_distributions,
192 ),
193 generate_list_getter(
194 "mithril_stake_distribution_list",
195 self.mithril_stake_distributions_list,
196 ),
197 generate_ids_array(
198 "cardano_stake_distribution_hashes",
199 BTreeSet::from_iter(
200 self.individual_cardano_stake_distributions.keys().cloned(),
201 ),
202 ),
203 generate_epoch_array(
204 "cardano_stake_distribution_epochs",
205 BTreeSet::from_iter(cardano_stake_distributions_per_epoch.keys().cloned()),
206 ),
207 generate_artifact_per_epoch_getter(
208 "cardano_stake_distributions_per_epoch",
209 extract_item_by_epoch(&self.individual_cardano_stake_distributions, "/epoch"),
210 ),
211 generate_artifact_getter(
212 "cardano_stake_distributions",
213 self.individual_cardano_stake_distributions,
214 ),
215 generate_list_getter(
216 "cardano_stake_distribution_list",
217 self.cardano_stake_distributions_list,
218 ),
219 generate_ids_array(
220 "certificate_hashes",
221 BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
222 ),
223 generate_ids_array(
224 "cardano_database_snapshot_hashes",
225 BTreeSet::from_iter(self.individual_cardano_database_snapshots.keys().cloned()),
226 ),
227 generate_epoch_array(
228 "cardano_database_snapshot_epochs",
229 BTreeSet::from_iter(cardano_database_snapshots_per_epoch.keys().cloned()),
230 ),
231 generate_artifact_getter(
232 "cardano_database_snapshots",
233 self.individual_cardano_database_snapshots,
234 ),
235 generate_list_getter(
236 "cardano_database_snapshot_list",
237 self.cardano_database_snapshots_list,
238 ),
239 generate_artifact_per_epoch_getter(
240 "cardano_database_snapshot_list_per_epoch",
241 cardano_database_snapshots_per_epoch,
242 ),
243 generate_artifact_getter("certificates", self.individual_certificates),
244 generate_list_getter("certificate_list", self.certificates_list),
245 generate_ids_array(
246 "cardano_transaction_snapshot_hashes",
247 BTreeSet::from_iter(
248 self.individual_cardano_transaction_snapshots.keys().cloned(),
249 ),
250 ),
251 generate_artifact_getter(
252 "cardano_transaction_snapshots",
253 self.individual_cardano_transaction_snapshots,
254 ),
255 generate_list_getter(
256 "cardano_transaction_snapshots_list",
257 self.cardano_transaction_snapshots_list,
258 ),
259 generate_ids_array(
260 "proof_transaction_hashes",
261 BTreeSet::from_iter(self.cardano_transaction_proofs.keys().cloned()),
262 ),
263 generate_artifact_getter(
264 "cardano_transaction_proofs",
265 self.cardano_transaction_proofs,
266 ),
267 ],
268 true,
269 )
270 }
271
272 fn assemble_code(functions_code: &[String], include_use_btree_map: bool) -> String {
273 format!(
274 "{}{}
275",
276 if include_use_btree_map {
277 "use std::collections::BTreeMap;
278
279"
280 } else {
281 ""
282 },
283 functions_code.join(
284 "
285
286"
287 )
288 )
289 }
290
291 fn read_artifacts_json_file(json_file: &Path) -> BTreeMap<ArtifactId, FileContent> {
292 let file = File::open(json_file).unwrap();
293 let parsed_json: serde_json::Value = serde_json::from_reader(&file).unwrap();
294
295 let json_object = parsed_json.as_object().unwrap();
296 let res: Result<Vec<_>, _> = json_object
297 .iter()
298 .map(|(key, value)| extract_artifact_id_and_content(key, value))
299 .collect();
300
301 BTreeMap::from_iter(res.unwrap())
302 }
303}
304
305fn extract_artifact_id_and_content(
306 key: &String,
307 value: &serde_json::Value,
308) -> Result<(ArtifactId, FileContent), String> {
309 let json_content = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
310 Ok((key.to_owned(), json_content))
311}
312
313pub fn extract_item_by_epoch(
317 items_per_hash: &BTreeMap<String, String>,
318 json_pointer_for_epoch: &str,
319) -> BTreeMap<u64, String> {
320 let mut res = BTreeMap::new();
321
322 for (key, value) in items_per_hash {
323 let parsed_json: serde_json::Value = serde_json::from_str(value)
324 .unwrap_or_else(|_| panic!("Could not parse JSON entity '{key}'"));
325 let epoch = parsed_json
326 .pointer(json_pointer_for_epoch)
327 .unwrap_or_else(|| panic!("missing `{json_pointer_for_epoch}` for JSON entity '{key}'"))
328 .as_u64()
329 .unwrap_or_else(|| {
330 panic!("`{json_pointer_for_epoch}` is not a number for JSON entity '{key}'")
331 });
332 res.insert(epoch, value.clone());
333 }
334
335 res
336}
337
338pub fn extract_item_list_per_epoch(
342 source: &str,
343 json_pointer_for_epoch: &str,
344) -> BTreeMap<u64, String> {
345 let parsed_json: Vec<serde_json::Value> =
346 serde_json::from_str(source).expect("Failed to parse JSON list");
347 let mut list_per_epoch = BTreeMap::<u64, Vec<serde_json::Value>>::new();
348
349 for item in parsed_json {
350 let epoch = item
351 .pointer(json_pointer_for_epoch)
352 .unwrap_or_else(|| panic!("missing `{json_pointer_for_epoch}` for a json value"))
353 .as_u64()
354 .unwrap_or_else(|| panic!("`{json_pointer_for_epoch}` is not a number"));
355 list_per_epoch.entry(epoch).or_default().push(item);
356 }
357
358 list_per_epoch
359 .into_iter()
360 .map(|(k, v)| (k, serde_json::to_string(&v).unwrap()))
361 .collect()
362}
363
364pub fn list_json_files_in_folder(folder: &Path) -> impl Iterator<Item = fs::DirEntry> + '_ {
365 crate::list_files_in_folder(folder)
366 .filter(|e| e.file_name().to_string_lossy().ends_with(".json"))
367}
368
369pub fn generate_artifact_getter(
371 fun_name: &str,
372 source_jsons: BTreeMap<ArtifactId, FileContent>,
373) -> String {
374 let mut artifacts_list = String::new();
375
376 for (artifact_id, file_content) in source_jsons {
377 write!(
378 artifacts_list,
379 r###"
380 (
381 "{artifact_id}",
382 r#"{file_content}"#
383 ),"###
384 )
385 .unwrap();
386 }
387
388 format!(
389 r###"pub(crate) fn {fun_name}() -> BTreeMap<String, String> {{
390 [{artifacts_list}
391 ]
392 .into_iter()
393 .map(|(k, v)| (k.to_owned(), v.to_owned()))
394 .collect()
395}}"###
396 )
397}
398
399pub fn generate_artifact_per_epoch_getter(
401 fun_name: &str,
402 source_jsons: BTreeMap<u64, FileContent>,
403) -> String {
404 let mut artifacts_list = String::new();
405
406 for (artifact_id, file_content) in source_jsons {
407 write!(
408 artifacts_list,
409 r###"
410 (
411 {artifact_id},
412 r#"{file_content}"#
413 ),"###
414 )
415 .unwrap();
416 }
417
418 format!(
419 r###"pub(crate) fn {fun_name}() -> BTreeMap<u64, String> {{
420 [{artifacts_list}
421 ]
422 .into_iter()
423 .map(|(k, v)| (k.to_owned(), v.to_owned()))
424 .collect()
425}}"###
426 )
427}
428
429pub fn generate_list_getter(fun_name: &str, source_json: FileContent) -> String {
431 format!(
432 r###"pub(crate) fn {fun_name}() -> &'static str {{
433 r#"{source_json}"#
434}}"###
435 )
436}
437
438pub fn generate_ids_array(array_name: &str, ids: BTreeSet<ArtifactId>) -> String {
440 let mut ids_list = String::new();
441
442 for id in &ids {
443 write!(
444 ids_list,
445 r#"
446 "{id}","#
447 )
448 .unwrap();
449 }
450
451 format!(
452 r###"pub(crate) const fn {}<'a>() -> [&'a str; {}] {{
453 [{}
454 ]
455}}"###,
456 array_name,
457 ids.len(),
458 ids_list,
459 )
460}
461
462pub fn generate_epoch_array(array_name: &str, epoch: BTreeSet<u64>) -> String {
464 let mut ids_list = String::new();
465
466 for id in &epoch {
467 write!(
468 ids_list,
469 r#"
470 {id},"#
471 )
472 .unwrap();
473 }
474
475 format!(
476 r###"pub(crate) const fn {}() -> [u64; {}] {{
477 [{}
478 ]
479}}"###,
480 array_name,
481 epoch.len(),
482 ids_list,
483 )
484}
485
486#[cfg(test)]
487mod tests {
488 use crate::get_temp_dir;
489
490 use super::*;
491
492 #[test]
493 fn generate_ids_array_with_empty_data() {
494 assert_eq!(
495 generate_ids_array("snapshots_digests", BTreeSet::new()),
496 "pub(crate) const fn snapshots_digests<'a>() -> [&'a str; 0] {
497 [
498 ]
499}"
500 );
501 }
502
503 #[test]
504 fn generate_ids_array_with_non_empty_data() {
505 assert_eq!(
506 generate_ids_array(
507 "snapshots_digests",
508 BTreeSet::from_iter(["abc".to_string(), "def".to_string(), "hij".to_string()])
509 ),
510 r#"pub(crate) const fn snapshots_digests<'a>() -> [&'a str; 3] {
511 [
512 "abc",
513 "def",
514 "hij",
515 ]
516}"#
517 );
518 }
519
520 #[test]
521 fn assemble_code_with_btree_use() {
522 assert_eq!(
523 "use std::collections::BTreeMap;
524
525fn a() {}
526
527fn b() {}
528",
529 FakeAggregatorData::assemble_code(
530 &["fn a() {}".to_string(), "fn b() {}".to_string()],
531 true
532 )
533 )
534 }
535
536 #[test]
537 fn assemble_code_without_btree_use() {
538 assert_eq!(
539 "fn a() {}
540
541fn b() {}
542",
543 FakeAggregatorData::assemble_code(
544 &["fn a() {}".to_string(), "fn b() {}".to_string()],
545 false
546 )
547 )
548 }
549
550 #[test]
551 fn parse_artifacts_json_into_btree_of_key_and_pretty_sub_json() {
552 let dir = get_temp_dir("read_artifacts_json_file");
553 let file = dir.join("test.json");
554 fs::write(
555 &file,
556 r#"{
557 "hash1": { "name": "artifact1" },
558 "hash2": { "name": "artifact2" }
559}"#,
560 )
561 .unwrap();
562
563 let id_per_json = FakeAggregatorData::read_artifacts_json_file(&file);
564
565 let expected = BTreeMap::from([
566 (
567 "hash1".to_string(),
568 r#"{
569 "name": "artifact1"
570}"#
571 .to_string(),
572 ),
573 (
574 "hash2".to_string(),
575 r#"{
576 "name": "artifact2"
577}"#
578 .to_string(),
579 ),
580 ]);
581 assert_eq!(expected, id_per_json);
582 }
583
584 #[test]
585 fn test_extract_item_by_epoch_by_epoch_with_valid_data() {
586 let items_per_hash = BTreeMap::from([
587 (
588 "hash1".to_string(),
589 r#"{"bar":4,"epoch":3,"foo":"...","hash":"2"}"#.to_string(),
590 ),
591 (
592 "hash2".to_string(),
593 r#"{"bar":7,"epoch":2,"foo":"...","hash":"1"}"#.to_string(),
594 ),
595 ]);
596
597 let item_per_epoch = extract_item_by_epoch(&items_per_hash, "/epoch");
599 assert_eq!(
600 BTreeMap::from([
601 (3, items_per_hash.get("hash1").unwrap().to_string()),
602 (2, items_per_hash.get("hash2").unwrap().to_string())
603 ]),
604 item_per_epoch
605 )
606 }
607
608 #[test]
609 #[should_panic(expected = "Could not parse JSON entity 'csd-123'")]
610 fn test_extract_item_by_epoch_by_epoch_with_invalid_json() {
611 let mut items_per_hash = BTreeMap::new();
612 items_per_hash.insert(
613 "csd-123".to_string(),
614 r#""hash": "csd-123", "epoch": "123"#.to_string(),
615 );
616
617 extract_item_by_epoch(&items_per_hash, "/epoch");
618 }
619
620 #[test]
621 #[should_panic(expected = "missing `/epoch` for JSON entity 'csd-123'")]
622 fn test_extract_item_by_epoch_with_missing_epoch() {
623 let mut items_per_hash = BTreeMap::new();
624 items_per_hash.insert("csd-123".to_string(), r#"{"hash": "csd-123"}"#.to_string());
625
626 extract_item_by_epoch(&items_per_hash, "/epoch");
627 }
628
629 #[test]
630 fn test_extract_item_by_epoch_with_empty_map() {
631 let items_per_hash = BTreeMap::new();
632
633 let epochs = extract_item_by_epoch(&items_per_hash, "/epoch");
634
635 assert!(epochs.is_empty());
636 }
637
638 #[test]
639 fn test_extract_item_list_per_epoch_for_epoch() {
640 let list_per_epoch_json = r#"[
641 { "beacon": { "epoch": 1, "bar": 4 }, "hash":"3","foo":"..." },
642 { "beacon": { "epoch": 2}, "hash":"2","foo":"..." },
643 { "beacon": { "epoch": 1}, "hash":"1","foo":"..." }
644 ]"#;
645
646 let map_per_epoch = extract_item_list_per_epoch(list_per_epoch_json, "/beacon/epoch");
648 assert_eq!(
649 BTreeMap::from([
650 (
651 1,
652 r#"[{"beacon":{"bar":4,"epoch":1},"foo":"...","hash":"3"},{"beacon":{"epoch":1},"foo":"...","hash":"1"}]"#
653 .to_string()
654 ),
655 (2, r#"[{"beacon":{"epoch":2},"foo":"...","hash":"2"}]"#.to_string()),
656 ]),
657 map_per_epoch
658 )
659 }
660
661 #[test]
662 #[should_panic(expected = "Failed to parse JSON list")]
663 fn test_extract_item_list_per_epoch_with_invalid_json() {
664 let list_per_epoch_json =
666 r#"[ { "beacon": { "epoch": 1, "bar": 4 }, "hash":"3","foo":"..." }, ]"#;
667
668 extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
669 }
670
671 #[test]
672 #[should_panic(expected = "missing `/epoch` for a json value")]
673 fn test_extract_item_list_per_epoch_with_missing_epoch() {
674 let list_per_epoch_json = r#"[ { "beacon": { "bar": 4 }, "hash":"3","foo":"..." } ]"#;
675
676 extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
677 }
678
679 #[test]
680 fn test_extract_item_list_per_epoch_with_list() {
681 let list_per_epoch_json = "[]";
682
683 let epochs = extract_item_list_per_epoch(list_per_epoch_json, "/epoch");
684
685 assert!(epochs.is_empty());
686 }
687}