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