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