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_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                // unknown file
105                _ => {}
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
313/// Takes a map of json string indexed by hashes and re-indexes them using their epoch
314///
315/// Each item in the map must contain an epoch value at the specified JSON pointer location.
316pub 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
338/// Takes a JSON string containing a list of items and extracts them into a map keyed by epoch.
339///
340/// Each item in the list must contain an epoch value at the specified JSON pointer location.
341pub 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
369// pub(crate) fn $fun_name()() -> BTreeMap<String, String>
370pub 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
399// pub(crate) fn $fun_name()() -> BTreeMap<u64, String>
400pub 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
429/// pub(crate) fn $fun_name() -> &'static str
430pub 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
438/// pub(crate) fn $array_name() -> [&'a str; $ids.len]
439pub 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
462/// pub(crate) fn $array_name() -> [u64; $epoch.len]
463pub 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        // note: values are not re-serialized, so they are kept as is
598        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        // note: values are re-serialized, so serde_json reorders the keys
647        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        // invalid because of the trailing comma
665        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}