mithril_client_cli/commands/cardano_db/
show.rs

1use anyhow::{anyhow, Context};
2use clap::Parser;
3use cli_table::{print_stdout, Cell, CellStruct, Table};
4
5use mithril_client::{
6    common::{AncillaryLocation, DigestLocation, ImmutablesLocation, MultiFilesUri},
7    Client, MithrilResult,
8};
9
10use crate::{
11    commands::{
12        cardano_db::CardanoDbCommandsBackend, client_builder_with_fallback_genesis_key, SharedArgs,
13    },
14    utils::{CardanoDbUtils, ExpanderUtils},
15    CommandContext,
16};
17
18/// Clap command to show a given Cardano db
19#[derive(Parser, Debug, Clone)]
20pub struct CardanoDbShowCommand {
21    #[clap(flatten)]
22    shared_args: SharedArgs,
23
24    #[arg(short, long, value_enum, default_value_t)]
25    backend: CardanoDbCommandsBackend,
26
27    /// Digest of the Cardano db snapshot to show or `latest` for the latest artifact
28    digest: String,
29}
30
31impl CardanoDbShowCommand {
32    /// Is JSON output enabled
33    pub fn is_json_output_enabled(&self) -> bool {
34        self.shared_args.json
35    }
36
37    /// Cardano DB Show command
38    pub async fn execute(&self, context: CommandContext) -> MithrilResult<()> {
39        let params = context.config_parameters()?;
40        let client = client_builder_with_fallback_genesis_key(&params)?
41            .with_logger(context.logger().clone())
42            .build()?;
43
44        match self.backend {
45            CardanoDbCommandsBackend::V1 => self.print_v1(client).await?,
46            CardanoDbCommandsBackend::V2 => self.print_v2(client).await?,
47        }
48
49        Ok(())
50    }
51
52    async fn print_v1(&self, client: Client) -> MithrilResult<()> {
53        let get_list_of_artifact_ids = || async {
54            let cardano_dbs = client.cardano_database().list().await.with_context(|| {
55                "Can not get the list of artifacts while retrieving the latest cardano db digest"
56            })?;
57
58            Ok(cardano_dbs
59                .iter()
60                .map(|cardano_db| cardano_db.digest.to_owned())
61                .collect::<Vec<String>>())
62        };
63
64        let cardano_db_message = client
65            .cardano_database()
66            .get(
67                &ExpanderUtils::expand_eventual_id_alias(&self.digest, get_list_of_artifact_ids())
68                    .await?,
69            )
70            .await?
71            .ok_or_else(|| anyhow!("Cardano DB not found for digest: '{}'", &self.digest))?;
72
73        if self.is_json_output_enabled() {
74            println!("{}", serde_json::to_string(&cardano_db_message)?);
75        } else {
76            let cardano_db_table = vec![
77                vec![
78                    "Epoch".cell(),
79                    format!("{}", &cardano_db_message.beacon.epoch).cell(),
80                ],
81                vec![
82                    "Immutable File Number".cell(),
83                    format!("{}", &cardano_db_message.beacon.immutable_file_number).cell(),
84                ],
85                vec!["Network".cell(), cardano_db_message.network.cell()],
86                vec!["Digest".cell(), cardano_db_message.digest.cell()],
87                vec![
88                    "Size".cell(),
89                    CardanoDbUtils::format_bytes_to_gigabytes(cardano_db_message.size).cell(),
90                ],
91                vec![
92                    "Cardano node version".cell(),
93                    cardano_db_message.cardano_node_version.cell(),
94                ],
95                vec![
96                    "Location".cell(),
97                    cardano_db_message.locations.join(",").cell(),
98                ],
99                vec![
100                    "Created".cell(),
101                    cardano_db_message.created_at.to_string().cell(),
102                ],
103                vec![
104                    "Compression Algorithm".cell(),
105                    format!("{}", &cardano_db_message.compression_algorithm).cell(),
106                ],
107            ]
108            .table();
109
110            print_stdout(cardano_db_table)?
111        }
112
113        Ok(())
114    }
115
116    async fn print_v2(&self, client: Client) -> MithrilResult<()> {
117        let get_list_of_artifact_ids = || async {
118            let cardano_dbs = client.cardano_database_v2().list().await.with_context(|| {
119                "Can not get the list of artifacts while retrieving the latest cardano db snapshot hash"
120            })?;
121
122            Ok(cardano_dbs
123                .iter()
124                .map(|cardano_db| cardano_db.hash.to_owned())
125                .collect::<Vec<String>>())
126        };
127
128        let cardano_db_message = client
129            .cardano_database_v2()
130            .get(
131                &ExpanderUtils::expand_eventual_id_alias(&self.digest, get_list_of_artifact_ids())
132                    .await?,
133            )
134            .await?
135            .ok_or_else(|| anyhow!("Cardano DB snapshot not found for hash: '{}'", &self.digest))?;
136
137        if self.is_json_output_enabled() {
138            println!("{}", serde_json::to_string(&cardano_db_message)?);
139        } else {
140            let mut cardano_db_table = vec![
141                vec![
142                    "Epoch".cell(),
143                    format!("{}", &cardano_db_message.beacon.epoch).cell(),
144                ],
145                vec![
146                    "Immutable File Number".cell(),
147                    format!("{}", &cardano_db_message.beacon.immutable_file_number).cell(),
148                ],
149                vec!["Hash".cell(), cardano_db_message.hash.cell()],
150                vec!["Merkle root".cell(), cardano_db_message.merkle_root.cell()],
151                vec![
152                    "Database size".cell(),
153                    CardanoDbUtils::format_bytes_to_gigabytes(
154                        cardano_db_message.total_db_size_uncompressed,
155                    )
156                    .cell(),
157                ],
158                vec![
159                    "Cardano node version".cell(),
160                    cardano_db_message.cardano_node_version.cell(),
161                ],
162            ];
163
164            cardano_db_table.append(&mut digest_location_rows(
165                &cardano_db_message.digests.locations,
166            ));
167
168            cardano_db_table.append(&mut immutables_location_rows(
169                &cardano_db_message.immutables.locations,
170            ));
171
172            cardano_db_table.append(&mut ancillary_location_rows(
173                &cardano_db_message.ancillary.locations,
174            ));
175
176            cardano_db_table.push(vec![
177                "Created".cell(),
178                cardano_db_message.created_at.to_string().cell(),
179            ]);
180
181            print_stdout(cardano_db_table.table())?;
182        }
183
184        Ok(())
185    }
186}
187
188fn digest_location_iter(locations: &[DigestLocation]) -> impl Iterator<Item = String> + use<'_> {
189    locations.iter().filter_map(|location| match location {
190        DigestLocation::Aggregator { uri } => Some(format!("Aggregator, uri: \"{uri}\"")),
191        DigestLocation::CloudStorage {
192            uri,
193            compression_algorithm: _,
194        } => Some(format!("CloudStorage, uri: \"{uri}\"")),
195        DigestLocation::Unknown => None,
196    })
197}
198
199fn digest_location_rows(locations: &[DigestLocation]) -> Vec<Vec<CellStruct>> {
200    format_location_rows("Digest location", digest_location_iter(locations))
201}
202
203fn immutables_location_iter(
204    locations: &[ImmutablesLocation],
205) -> impl Iterator<Item = String> + use<'_> {
206    locations.iter().filter_map(|location| match location {
207        ImmutablesLocation::CloudStorage {
208            uri,
209            compression_algorithm: _,
210        } => match uri {
211            MultiFilesUri::Template(template_uri) => Some(format!(
212                "CloudStorage, template_uri: \"{}\"",
213                template_uri.0
214            )),
215        },
216        ImmutablesLocation::Unknown => None,
217    })
218}
219
220fn immutables_location_rows(locations: &[ImmutablesLocation]) -> Vec<Vec<CellStruct>> {
221    format_location_rows("Immutables location", immutables_location_iter(locations))
222}
223
224fn ancillary_location_iter(
225    locations: &[AncillaryLocation],
226) -> impl Iterator<Item = String> + use<'_> {
227    locations.iter().filter_map(|location| match location {
228        AncillaryLocation::CloudStorage {
229            uri,
230            compression_algorithm: _,
231        } => Some(format!("CloudStorage, uri: \"{uri}\"")),
232        AncillaryLocation::Unknown => None,
233    })
234}
235
236fn ancillary_location_rows(locations: &[AncillaryLocation]) -> Vec<Vec<CellStruct>> {
237    format_location_rows("Ancillary location", ancillary_location_iter(locations))
238}
239
240fn format_location_rows(
241    location_name: &str,
242    locations: impl Iterator<Item = String>,
243) -> Vec<Vec<CellStruct>> {
244    locations
245        .enumerate()
246        .map(|(index, cell_content)| {
247            vec![
248                format!("{location_name} ({})", index + 1).cell(),
249                cell_content.cell(),
250            ]
251        })
252        .collect()
253}
254
255#[cfg(test)]
256mod tests {
257    use mithril_client::common::{CompressionAlgorithm, TemplateUri};
258
259    use super::*;
260
261    #[test]
262    fn digest_location_rows_when_no_uri_found() {
263        let rows = digest_location_rows(&[]);
264
265        assert!(rows.is_empty());
266    }
267
268    #[test]
269    fn digest_location_rows_when_uris_found() {
270        let locations = vec![
271            DigestLocation::Aggregator {
272                uri: "http://aggregator.net/".to_string(),
273            },
274            DigestLocation::CloudStorage {
275                uri: "http://cloudstorage.com/".to_string(),
276                compression_algorithm: None,
277            },
278        ];
279
280        let rows = digest_location_rows(&locations);
281        assert_eq!(rows.len(), 2);
282
283        let table = rows.table();
284        let rows_rendered = table.display().unwrap().to_string();
285
286        assert!(rows_rendered.contains("Digest location (1)"));
287        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage.com/\""));
288        assert!(rows_rendered.contains("Digest location (2)"));
289        assert!(rows_rendered.contains("Aggregator, uri: \"http://aggregator.net/\""));
290    }
291
292    #[test]
293    fn digest_location_rows_display_and_count_only_known_location() {
294        let locations = vec![
295            DigestLocation::Unknown,
296            DigestLocation::CloudStorage {
297                uri: "http://cloudstorage.com/".to_string(),
298                compression_algorithm: None,
299            },
300        ];
301
302        let rows = digest_location_rows(&locations);
303        assert_eq!(1, rows.len());
304
305        let rows_rendered = rows.table().display().unwrap().to_string();
306        assert!(rows_rendered.contains("Digest location (1)"));
307    }
308
309    #[test]
310    fn immutables_location_rows_when_no_uri_found() {
311        let rows = immutables_location_rows(&[]);
312
313        assert!(rows.is_empty());
314    }
315
316    #[test]
317    fn immutables_location_row_returns_some_when_uri_found() {
318        let locations = vec![
319            ImmutablesLocation::CloudStorage {
320                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage1.com/".to_string())),
321                compression_algorithm: Some(CompressionAlgorithm::Gzip),
322            },
323            ImmutablesLocation::CloudStorage {
324                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
325                compression_algorithm: Some(CompressionAlgorithm::Gzip),
326            },
327        ];
328
329        let rows = immutables_location_rows(&locations);
330
331        assert_eq!(rows.len(), 2);
332
333        let table = rows.table();
334        let rows_rendered = table.display().unwrap().to_string();
335
336        assert!(rows_rendered.contains("Immutables location (1)"));
337        assert!(rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage1.com/\""));
338        assert!(rows_rendered.contains("Immutables location (2)"));
339        assert!(rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage2.com/\""));
340    }
341
342    #[test]
343    fn immutables_location_row_display_and_count_only_known_location() {
344        let locations = vec![
345            ImmutablesLocation::Unknown {},
346            ImmutablesLocation::CloudStorage {
347                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
348                compression_algorithm: Some(CompressionAlgorithm::Gzip),
349            },
350        ];
351
352        let rows = immutables_location_rows(&locations);
353        assert_eq!(1, rows.len());
354
355        let rows_rendered = rows.table().display().unwrap().to_string();
356        assert!(rows_rendered.contains("Immutables location (1)"));
357    }
358
359    #[test]
360    fn ancillary_location_rows_when_no_uri_found() {
361        let rows = ancillary_location_rows(&[]);
362
363        assert!(rows.is_empty());
364    }
365
366    #[test]
367    fn ancillary_location_rows_when_uris_found() {
368        let locations = vec![
369            AncillaryLocation::CloudStorage {
370                uri: "http://cloudstorage1.com/".to_string(),
371                compression_algorithm: Some(CompressionAlgorithm::Gzip),
372            },
373            AncillaryLocation::CloudStorage {
374                uri: "http://cloudstorage2.com/".to_string(),
375                compression_algorithm: Some(CompressionAlgorithm::Gzip),
376            },
377        ];
378
379        let rows = ancillary_location_rows(&locations);
380
381        assert_eq!(rows.len(), 2);
382
383        let table = rows.table();
384        let rows_rendered = table.display().unwrap().to_string();
385
386        assert!(rows_rendered.contains("Ancillary location (1)"));
387        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage1.com/\""));
388        assert!(rows_rendered.contains("Ancillary location (2)"));
389        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage2.com/\""));
390    }
391
392    #[test]
393    fn ancillary_location_rows_display_and_count_only_known_location() {
394        let locations = vec![
395            AncillaryLocation::Unknown {},
396            AncillaryLocation::CloudStorage {
397                uri: "http://cloudstorage2.com/".to_string(),
398                compression_algorithm: Some(CompressionAlgorithm::Gzip),
399            },
400        ];
401
402        let rows = ancillary_location_rows(&locations);
403        assert_eq!(1, rows.len());
404
405        let rows_rendered = rows.table().display().unwrap().to_string();
406        assert!(rows_rendered.contains("Ancillary location (1)"));
407    }
408}