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