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