mithril_client_cli/commands/cardano_db/
show.rs

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