mithril_client_cli/commands/cardano_db_v2/
show.rs

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