1use anyhow::Context;
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 .with_context(|| format!("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 .with_context(|| {
141 format!("Cardano DB snapshot not found for hash: '{}'", self.digest)
142 })?;
143
144 if context.is_json_output_enabled() {
145 println!("{}", serde_json::to_string(&cardano_db_message)?);
146 } else {
147 let mut cardano_db_table = vec![
148 vec!["Epoch".cell(), format!("{}", &cardano_db_message.beacon.epoch).cell()],
149 vec![
150 "Immutable File Number".cell(),
151 format!("{}", &cardano_db_message.beacon.immutable_file_number).cell(),
152 ],
153 vec!["Hash".cell(), cardano_db_message.hash.cell()],
154 vec!["Merkle root".cell(), cardano_db_message.merkle_root.cell()],
155 vec![
156 "Database size".cell(),
157 CardanoDbUtils::format_bytes_to_gigabytes(
158 cardano_db_message.total_db_size_uncompressed,
159 )
160 .cell(),
161 ],
162 vec![
163 "Cardano node version".cell(),
164 cardano_db_message.cardano_node_version.cell(),
165 ],
166 ];
167
168 cardano_db_table.append(&mut digest_location_rows(
169 &cardano_db_message.digests.locations,
170 ));
171
172 cardano_db_table.append(&mut immutables_location_rows(
173 &cardano_db_message.immutables.locations,
174 ));
175
176 cardano_db_table.append(&mut ancillary_location_rows(
177 &cardano_db_message.ancillary.locations,
178 ));
179
180 cardano_db_table.push(vec![
181 "Created".cell(),
182 cardano_db_message.created_at.to_string().cell(),
183 ]);
184
185 print_stdout(cardano_db_table.table())?;
186 }
187
188 Ok(())
189 }
190}
191
192fn digest_location_iter(locations: &[DigestLocation]) -> impl Iterator<Item = String> + use<'_> {
193 locations.iter().filter_map(|location| match location {
194 DigestLocation::Aggregator { uri } => Some(format!("Aggregator, uri: \"{uri}\"")),
195 DigestLocation::CloudStorage {
196 uri,
197 compression_algorithm: _,
198 } => Some(format!("CloudStorage, uri: \"{uri}\"")),
199 DigestLocation::Unknown => None,
200 })
201}
202
203fn digest_location_rows(locations: &[DigestLocation]) -> Vec<Vec<CellStruct>> {
204 format_location_rows("Digest location", digest_location_iter(locations))
205}
206
207fn immutables_location_iter(
208 locations: &[ImmutablesLocation],
209) -> impl Iterator<Item = String> + use<'_> {
210 locations.iter().filter_map(|location| match location {
211 ImmutablesLocation::CloudStorage {
212 uri,
213 compression_algorithm: _,
214 } => match uri {
215 MultiFilesUri::Template(template_uri) => Some(format!(
216 "CloudStorage, template_uri: \"{}\"",
217 template_uri.0
218 )),
219 },
220 ImmutablesLocation::Unknown => None,
221 })
222}
223
224fn immutables_location_rows(locations: &[ImmutablesLocation]) -> Vec<Vec<CellStruct>> {
225 format_location_rows("Immutables location", immutables_location_iter(locations))
226}
227
228fn ancillary_location_iter(
229 locations: &[AncillaryLocation],
230) -> impl Iterator<Item = String> + use<'_> {
231 locations.iter().filter_map(|location| match location {
232 AncillaryLocation::CloudStorage {
233 uri,
234 compression_algorithm: _,
235 } => Some(format!("CloudStorage, uri: \"{uri}\"")),
236 AncillaryLocation::Unknown => None,
237 })
238}
239
240fn ancillary_location_rows(locations: &[AncillaryLocation]) -> Vec<Vec<CellStruct>> {
241 format_location_rows("Ancillary location", ancillary_location_iter(locations))
242}
243
244fn format_location_rows(
245 location_name: &str,
246 locations: impl Iterator<Item = String>,
247) -> Vec<Vec<CellStruct>> {
248 locations
249 .enumerate()
250 .map(|(index, cell_content)| {
251 vec![format!("{location_name} ({})", index + 1).cell(), cell_content.cell()]
252 })
253 .collect()
254}
255
256#[cfg(test)]
257mod tests {
258 use mithril_client::common::{CompressionAlgorithm, TemplateUri};
259
260 use super::*;
261
262 #[test]
263 fn digest_location_rows_when_no_uri_found() {
264 let rows = digest_location_rows(&[]);
265
266 assert!(rows.is_empty());
267 }
268
269 #[test]
270 fn digest_location_rows_when_uris_found() {
271 let locations = vec![
272 DigestLocation::Aggregator {
273 uri: "http://aggregator.net/".to_string(),
274 },
275 DigestLocation::CloudStorage {
276 uri: "http://cloudstorage.com/".to_string(),
277 compression_algorithm: None,
278 },
279 ];
280
281 let rows = digest_location_rows(&locations);
282 assert_eq!(rows.len(), 2);
283
284 let table = rows.table();
285 let rows_rendered = table.display().unwrap().to_string();
286
287 assert!(rows_rendered.contains("Digest location (1)"));
288 assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage.com/\""));
289 assert!(rows_rendered.contains("Digest location (2)"));
290 assert!(rows_rendered.contains("Aggregator, uri: \"http://aggregator.net/\""));
291 }
292
293 #[test]
294 fn digest_location_rows_display_and_count_only_known_location() {
295 let locations = vec![
296 DigestLocation::Unknown,
297 DigestLocation::CloudStorage {
298 uri: "http://cloudstorage.com/".to_string(),
299 compression_algorithm: None,
300 },
301 ];
302
303 let rows = digest_location_rows(&locations);
304 assert_eq!(1, rows.len());
305
306 let rows_rendered = rows.table().display().unwrap().to_string();
307 assert!(rows_rendered.contains("Digest location (1)"));
308 }
309
310 #[test]
311 fn immutables_location_rows_when_no_uri_found() {
312 let rows = immutables_location_rows(&[]);
313
314 assert!(rows.is_empty());
315 }
316
317 #[test]
318 fn immutables_location_row_returns_some_when_uri_found() {
319 let locations = vec![
320 ImmutablesLocation::CloudStorage {
321 uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage1.com/".to_string())),
322 compression_algorithm: Some(CompressionAlgorithm::Gzip),
323 },
324 ImmutablesLocation::CloudStorage {
325 uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
326 compression_algorithm: Some(CompressionAlgorithm::Gzip),
327 },
328 ];
329
330 let rows = immutables_location_rows(&locations);
331
332 assert_eq!(rows.len(), 2);
333
334 let table = rows.table();
335 let rows_rendered = table.display().unwrap().to_string();
336
337 assert!(rows_rendered.contains("Immutables location (1)"));
338 assert!(
339 rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage1.com/\"")
340 );
341 assert!(rows_rendered.contains("Immutables location (2)"));
342 assert!(
343 rows_rendered.contains("CloudStorage, template_uri: \"http://cloudstorage2.com/\"")
344 );
345 }
346
347 #[test]
348 fn immutables_location_row_display_and_count_only_known_location() {
349 let locations = vec![
350 ImmutablesLocation::Unknown {},
351 ImmutablesLocation::CloudStorage {
352 uri: MultiFilesUri::Template(TemplateUri("http://cloudstorage2.com/".to_string())),
353 compression_algorithm: Some(CompressionAlgorithm::Gzip),
354 },
355 ];
356
357 let rows = immutables_location_rows(&locations);
358 assert_eq!(1, rows.len());
359
360 let rows_rendered = rows.table().display().unwrap().to_string();
361 assert!(rows_rendered.contains("Immutables location (1)"));
362 }
363
364 #[test]
365 fn ancillary_location_rows_when_no_uri_found() {
366 let rows = ancillary_location_rows(&[]);
367
368 assert!(rows.is_empty());
369 }
370
371 #[test]
372 fn ancillary_location_rows_when_uris_found() {
373 let locations = vec![
374 AncillaryLocation::CloudStorage {
375 uri: "http://cloudstorage1.com/".to_string(),
376 compression_algorithm: Some(CompressionAlgorithm::Gzip),
377 },
378 AncillaryLocation::CloudStorage {
379 uri: "http://cloudstorage2.com/".to_string(),
380 compression_algorithm: Some(CompressionAlgorithm::Gzip),
381 },
382 ];
383
384 let rows = ancillary_location_rows(&locations);
385
386 assert_eq!(rows.len(), 2);
387
388 let table = rows.table();
389 let rows_rendered = table.display().unwrap().to_string();
390
391 assert!(rows_rendered.contains("Ancillary location (1)"));
392 assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage1.com/\""));
393 assert!(rows_rendered.contains("Ancillary location (2)"));
394 assert!(rows_rendered.contains("CloudStorage, uri: \"http://cloudstorage2.com/\""));
395 }
396
397 #[test]
398 fn ancillary_location_rows_display_and_count_only_known_location() {
399 let locations = vec![
400 AncillaryLocation::Unknown {},
401 AncillaryLocation::CloudStorage {
402 uri: "http://cloudstorage2.com/".to_string(),
403 compression_algorithm: Some(CompressionAlgorithm::Gzip),
404 },
405 ];
406
407 let rows = ancillary_location_rows(&locations);
408 assert_eq!(1, rows.len());
409
410 let rows_rendered = rows.table().display().unwrap().to_string();
411 assert!(rows_rendered.contains("Ancillary location (1)"));
412 }
413}