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