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