mithril_build_script/
fake_aggregator.rs

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/// In memory representation of a folder containing data imported using the `scripts/import.sh` script
14/// of the fake aggregator.
15#[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                // unknown file
115                _ => {}
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
337/// Takes a map of json string indexed by hashes and re-indexes them using their epoch
338///
339/// Each item in the map must contain an epoch value at the specified JSON pointer location.
340pub 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
362/// Takes a JSON string containing a list of items and extracts them into a map keyed by epoch.
363///
364/// Each item in the list must contain an epoch value at the specified JSON pointer location.
365pub 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
393// pub(crate) fn $fun_name()() -> BTreeMap<String, String>
394pub 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
423// pub(crate) fn $fun_name()() -> BTreeMap<u64, String>
424pub 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
453/// pub(crate) fn $fun_name() -> &'static str
454pub 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
462/// pub(crate) fn $array_name() -> [&'a str; $ids.len]
463pub 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
486/// pub(crate) fn $array_name() -> [u64; $epoch.len]
487pub 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        // note: values are not re-serialized, so they are kept as is
622        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        // note: values are re-serialized, so serde_json reorders the keys
671        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        // invalid because of the trailing comma
689        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}