mithril_client_cli/commands/cardano_db/
show.rs

1use anyhow::Context;
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            .with_context(|| format!("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            .with_context(|| {
141                format!("Cardano DB snapshot not found for hash: '{}'", self.digest)
142            })?;
143
144        if context.is_json_output_enabled() {
145            println!("{}", serde_json::to_string(&cardano_db_message)?);
146        } else {
147            let mut cardano_db_table = vec![
148                vec!["Epoch".cell(), format!("{}", &cardano_db_message.beacon.epoch).cell()],
149                vec![
150                    "Immutable File Number".cell(),
151                    format!("{}", &cardano_db_message.beacon.immutable_file_number).cell(),
152                ],
153                vec!["Hash".cell(), cardano_db_message.hash.cell()],
154                vec!["Merkle root".cell(), cardano_db_message.merkle_root.cell()],
155                vec![
156                    "Database size".cell(),
157                    CardanoDbUtils::format_bytes_to_gigabytes(
158                        cardano_db_message.total_db_size_uncompressed,
159                    )
160                    .cell(),
161                ],
162                vec![
163                    "Cardano node version".cell(),
164                    cardano_db_message.cardano_node_version.cell(),
165                ],
166            ];
167
168            cardano_db_table.append(&mut digest_location_rows(
169                &cardano_db_message.digests.locations,
170            ));
171
172            cardano_db_table.append(&mut immutables_location_rows(
173                &cardano_db_message.immutables.locations,
174            ));
175
176            cardano_db_table.append(&mut ancillary_location_rows(
177                &cardano_db_message.ancillary.locations,
178            ));
179
180            cardano_db_table.push(vec![
181                "Created".cell(),
182                cardano_db_message.created_at.to_string().cell(),
183            ]);
184
185            print_stdout(cardano_db_table.table())?;
186        }
187
188        Ok(())
189    }
190}
191
192fn digest_location_iter(locations: &[DigestLocation]) -> impl Iterator<Item = String> + use<'_> {
193    locations.iter().filter_map(|location| match location {
194        DigestLocation::Aggregator { uri } => Some(format!("Aggregator, uri: \"{uri}\"")),
195        DigestLocation::CloudStorage {
196            uri,
197            compression_algorithm: _,
198        } => Some(format!("CloudStorage, uri: \"{uri}\"")),
199        DigestLocation::Unknown => None,
200    })
201}
202
203fn digest_location_rows(locations: &[DigestLocation]) -> Vec<Vec<CellStruct>> {
204    format_location_rows("Digest location", digest_location_iter(locations))
205}
206
207fn immutables_location_iter(
208    locations: &[ImmutablesLocation],
209) -> impl Iterator<Item = String> + use<'_> {
210    locations.iter().filter_map(|location| match location {
211        ImmutablesLocation::CloudStorage {
212            uri,
213            compression_algorithm: _,
214        } => match uri {
215            MultiFilesUri::Template(template_uri) => Some(format!(
216                "CloudStorage, template_uri: \"{}\"",
217                template_uri.0
218            )),
219        },
220        ImmutablesLocation::Unknown => None,
221    })
222}
223
224fn immutables_location_rows(locations: &[ImmutablesLocation]) -> Vec<Vec<CellStruct>> {
225    format_location_rows("Immutables location", immutables_location_iter(locations))
226}
227
228fn ancillary_location_iter(
229    locations: &[AncillaryLocation],
230) -> impl Iterator<Item = String> + use<'_> {
231    locations.iter().filter_map(|location| match location {
232        AncillaryLocation::CloudStorage {
233            uri,
234            compression_algorithm: _,
235        } => Some(format!("CloudStorage, uri: \"{uri}\"")),
236        AncillaryLocation::Unknown => None,
237    })
238}
239
240fn ancillary_location_rows(locations: &[AncillaryLocation]) -> Vec<Vec<CellStruct>> {
241    format_location_rows("Ancillary location", ancillary_location_iter(locations))
242}
243
244fn format_location_rows(
245    location_name: &str,
246    locations: impl Iterator<Item = String>,
247) -> Vec<Vec<CellStruct>> {
248    locations
249        .enumerate()
250        .map(|(index, cell_content)| {
251            vec![format!("{location_name} ({})", index + 1).cell(), cell_content.cell()]
252        })
253        .collect()
254}
255
256#[cfg(test)]
257mod tests {
258    use mithril_client::common::{CompressionAlgorithm, TemplateUri};
259
260    use super::*;
261
262    #[test]
263    fn digest_location_rows_when_no_uri_found() {
264        let rows = digest_location_rows(&[]);
265
266        assert!(rows.is_empty());
267    }
268
269    #[test]
270    fn digest_location_rows_when_uris_found() {
271        let locations = vec![
272            DigestLocation::Aggregator {
273                uri: "http://aggregator.net/".to_string(),
274            },
275            DigestLocation::CloudStorage {
276                uri: "http://cloudstorage.com/".to_string(),
277                compression_algorithm: None,
278            },
279        ];
280
281        let rows = digest_location_rows(&locations);
282        assert_eq!(rows.len(), 2);
283
284        let table = rows.table();
285        let rows_rendered = table.display().unwrap().to_string();
286
287        assert!(rows_rendered.contains("Digest location (1)"));
288        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage.com/\""));
289        assert!(rows_rendered.contains("Digest location (2)"));
290        assert!(rows_rendered.contains("Aggregator, uri: \"http://aggregator.net/\""));
291    }
292
293    #[test]
294    fn digest_location_rows_display_and_count_only_known_location() {
295        let locations = vec![
296            DigestLocation::Unknown,
297            DigestLocation::CloudStorage {
298                uri: "http://cloudstorage.com/".to_string(),
299                compression_algorithm: None,
300            },
301        ];
302
303        let rows = digest_location_rows(&locations);
304        assert_eq!(1, rows.len());
305
306        let rows_rendered = rows.table().display().unwrap().to_string();
307        assert!(rows_rendered.contains("Digest location (1)"));
308    }
309
310    #[test]
311    fn immutables_location_rows_when_no_uri_found() {
312        let rows = immutables_location_rows(&[]);
313
314        assert!(rows.is_empty());
315    }
316
317    #[test]
318    fn immutables_location_row_returns_some_when_uri_found() {
319        let locations = vec![
320            ImmutablesLocation::CloudStorage {
321                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage1.com/".to_string())),
322                compression_algorithm: Some(CompressionAlgorithm::Gzip),
323            },
324            ImmutablesLocation::CloudStorage {
325                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
326                compression_algorithm: Some(CompressionAlgorithm::Gzip),
327            },
328        ];
329
330        let rows = immutables_location_rows(&locations);
331
332        assert_eq!(rows.len(), 2);
333
334        let table = rows.table();
335        let rows_rendered = table.display().unwrap().to_string();
336
337        assert!(rows_rendered.contains("Immutables location (1)"));
338        assert!(
339            rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage1.com/\"")
340        );
341        assert!(rows_rendered.contains("Immutables location (2)"));
342        assert!(
343            rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage2.com/\"")
344        );
345    }
346
347    #[test]
348    fn immutables_location_row_display_and_count_only_known_location() {
349        let locations = vec![
350            ImmutablesLocation::Unknown {},
351            ImmutablesLocation::CloudStorage {
352                uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
353                compression_algorithm: Some(CompressionAlgorithm::Gzip),
354            },
355        ];
356
357        let rows = immutables_location_rows(&locations);
358        assert_eq!(1, rows.len());
359
360        let rows_rendered = rows.table().display().unwrap().to_string();
361        assert!(rows_rendered.contains("Immutables location (1)"));
362    }
363
364    #[test]
365    fn ancillary_location_rows_when_no_uri_found() {
366        let rows = ancillary_location_rows(&[]);
367
368        assert!(rows.is_empty());
369    }
370
371    #[test]
372    fn ancillary_location_rows_when_uris_found() {
373        let locations = vec![
374            AncillaryLocation::CloudStorage {
375                uri: "http://cloudstorage1.com/".to_string(),
376                compression_algorithm: Some(CompressionAlgorithm::Gzip),
377            },
378            AncillaryLocation::CloudStorage {
379                uri: "http://cloudstorage2.com/".to_string(),
380                compression_algorithm: Some(CompressionAlgorithm::Gzip),
381            },
382        ];
383
384        let rows = ancillary_location_rows(&locations);
385
386        assert_eq!(rows.len(), 2);
387
388        let table = rows.table();
389        let rows_rendered = table.display().unwrap().to_string();
390
391        assert!(rows_rendered.contains("Ancillary location (1)"));
392        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage1.com/\""));
393        assert!(rows_rendered.contains("Ancillary location (2)"));
394        assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage2.com/\""));
395    }
396
397    #[test]
398    fn ancillary_location_rows_display_and_count_only_known_location() {
399        let locations = vec![
400            AncillaryLocation::Unknown {},
401            AncillaryLocation::CloudStorage {
402                uri: "http://cloudstorage2.com/".to_string(),
403                compression_algorithm: Some(CompressionAlgorithm::Gzip),
404            },
405        ];
406
407        let rows = ancillary_location_rows(&locations);
408        assert_eq!(1, rows.len());
409
410        let rows_rendered = rows.table().display().unwrap().to_string();
411        assert!(rows_rendered.contains("Ancillary location (1)"));
412    }
413}