1use anyhow::anyhow;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::entities::{
6 AncillaryLocation, AncillaryLocations, CardanoDbBeacon, CompressionAlgorithm, DigestLocation,
7 DigestsLocations, Epoch, ImmutablesLocation, ImmutablesLocations, MultiFilesUri, TemplateUri,
8};
9use crate::StdResult;
10
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct DigestsMessagePart {
14 pub size_uncompressed: u64,
16
17 pub locations: Vec<DigestLocation>,
19}
20
21impl DigestsMessagePart {
22 pub fn sanitized_locations(&self) -> StdResult<Vec<DigestLocation>> {
24 let sanitized_locations: Vec<_> = self
25 .locations
26 .iter()
27 .filter(|l| !matches!(l, DigestLocation::Unknown))
28 .cloned()
29 .collect();
30
31 if sanitized_locations.is_empty() {
32 Err(anyhow!("All digests locations are unknown."))
33 } else {
34 Ok(sanitized_locations)
35 }
36 }
37}
38
39#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ImmutablesMessagePart {
42 pub average_size_uncompressed: u64,
44
45 pub locations: Vec<ImmutablesLocation>,
47}
48
49impl ImmutablesMessagePart {
50 pub fn sanitized_locations(&self) -> StdResult<Vec<ImmutablesLocation>> {
52 let sanitized_locations: Vec<_> = self
53 .locations
54 .iter()
55 .filter(|l| !matches!(l, ImmutablesLocation::Unknown))
56 .cloned()
57 .collect();
58
59 if sanitized_locations.is_empty() {
60 Err(anyhow!("All locations are unknown."))
61 } else {
62 Ok(sanitized_locations)
63 }
64 }
65}
66
67#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct AncillaryMessagePart {
70 pub size_uncompressed: u64,
72
73 pub locations: Vec<AncillaryLocation>,
75}
76
77impl AncillaryMessagePart {
78 pub fn sanitized_locations(&self) -> StdResult<Vec<AncillaryLocation>> {
80 let sanitized_locations: Vec<_> = self
81 .locations
82 .iter()
83 .filter(|l| !matches!(l, AncillaryLocation::Unknown))
84 .cloned()
85 .collect();
86
87 if sanitized_locations.is_empty() {
88 Err(anyhow!("All locations are unknown."))
89 } else {
90 Ok(sanitized_locations)
91 }
92 }
93}
94
95impl From<DigestsLocations> for DigestsMessagePart {
96 fn from(part: DigestsLocations) -> Self {
97 Self {
98 size_uncompressed: part.size_uncompressed,
99 locations: part.locations,
100 }
101 }
102}
103
104impl From<ImmutablesLocations> for ImmutablesMessagePart {
105 fn from(part: ImmutablesLocations) -> Self {
106 Self {
107 average_size_uncompressed: part.average_size_uncompressed,
108 locations: part.locations,
109 }
110 }
111}
112
113impl From<AncillaryLocations> for AncillaryMessagePart {
114 fn from(part: AncillaryLocations) -> Self {
115 Self {
116 size_uncompressed: part.size_uncompressed,
117 locations: part.locations,
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124pub struct CardanoDatabaseSnapshotMessage {
125 pub hash: String,
127
128 pub merkle_root: String,
130
131 pub network: String,
133
134 pub beacon: CardanoDbBeacon,
136
137 pub certificate_hash: String,
139
140 pub total_db_size_uncompressed: u64,
142
143 pub digests: DigestsMessagePart,
145
146 pub immutables: ImmutablesMessagePart,
148
149 pub ancillary: AncillaryMessagePart,
151
152 pub cardano_node_version: String,
154
155 pub created_at: DateTime<Utc>,
157}
158
159impl CardanoDatabaseSnapshotMessage {
160 pub fn dummy() -> Self {
162 Self {
163 hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
164 merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
165 .to_string(),
166 network: "preview".to_string(),
167 beacon: CardanoDbBeacon {
168 epoch: Epoch(123),
169 immutable_file_number: 2345,
170 },
171 certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
172 .to_string(),
173 total_db_size_uncompressed: 800796318,
174 created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
175 .unwrap()
176 .with_timezone(&Utc),
177 digests: DigestsMessagePart {
178 size_uncompressed: 1024,
179 locations: vec![DigestLocation::Aggregator {
180 uri: "https://host-1/digest-1".to_string(),
181 }],
182 },
183 immutables: ImmutablesMessagePart {
184 average_size_uncompressed: 512,
185 locations: vec![
186 ImmutablesLocation::CloudStorage {
187 uri: MultiFilesUri::Template(TemplateUri(
188 "https://host-1/immutables-2".to_string(),
189 )),
190 compression_algorithm: Some(CompressionAlgorithm::Gzip),
191 },
192 ImmutablesLocation::CloudStorage {
193 uri: MultiFilesUri::Template(TemplateUri(
194 "https://host-2/immutables-2".to_string(),
195 )),
196 compression_algorithm: Some(CompressionAlgorithm::Gzip),
197 },
198 ],
199 },
200 ancillary: AncillaryMessagePart {
201 size_uncompressed: 2048,
202 locations: vec![AncillaryLocation::CloudStorage {
203 uri: "https://host-1/ancillary-3".to_string(),
204 compression_algorithm: Some(CompressionAlgorithm::Gzip),
205 }],
206 },
207 cardano_node_version: "0.0.1".to_string(),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 const CURRENT_JSON: &str = r#"
217 {
218 "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
219 "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
220 "network": "preview",
221 "beacon": {
222 "epoch": 123,
223 "immutable_file_number": 2345
224 },
225 "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
226 "total_db_size_uncompressed": 800796318,
227 "digests": {
228 "size_uncompressed": 1024,
229 "locations": [
230 {
231 "type": "aggregator",
232 "uri": "https://host-1/digest-1"
233 }
234 ]
235 },
236 "immutables": {
237 "average_size_uncompressed": 2048,
238 "locations": [
239 {
240 "type": "cloud_storage",
241 "uri": {
242 "Template": "https://host-1/immutables-{immutable_file_number}"
243 },
244 "compression_algorithm": "gzip"
245 },
246 {
247 "type": "cloud_storage",
248 "uri": {
249 "Template": "https://host-2/immutables-{immutable_file_number}"
250 }
251 }
252 ]
253 },
254 "ancillary": {
255 "size_uncompressed": 4096,
256 "locations": [
257 {
258 "type": "cloud_storage",
259 "uri": "https://host-1/ancillary-3",
260 "compression_algorithm": "gzip"
261 }
262 ]
263 },
264 "cardano_node_version": "0.0.1",
265 "created_at": "2023-01-19T13:43:05.618857482Z"
266 }"#;
267
268 fn golden_current_message() -> CardanoDatabaseSnapshotMessage {
269 CardanoDatabaseSnapshotMessage {
270 hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
271 merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
272 .to_string(),
273 network: "preview".to_string(),
274 beacon: CardanoDbBeacon {
275 epoch: Epoch(123),
276 immutable_file_number: 2345,
277 },
278 certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
279 .to_string(),
280 total_db_size_uncompressed: 800796318,
281 created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
282 .unwrap()
283 .with_timezone(&Utc),
284 digests: DigestsMessagePart {
285 size_uncompressed: 1024,
286 locations: vec![DigestLocation::Aggregator {
287 uri: "https://host-1/digest-1".to_string(),
288 }],
289 },
290 immutables: ImmutablesMessagePart {
291 average_size_uncompressed: 2048,
292 locations: vec![
293 ImmutablesLocation::CloudStorage {
294 uri: MultiFilesUri::Template(TemplateUri(
295 "https://host-1/immutables-{immutable_file_number}".to_string(),
296 )),
297 compression_algorithm: Some(CompressionAlgorithm::Gzip),
298 },
299 ImmutablesLocation::CloudStorage {
300 uri: MultiFilesUri::Template(TemplateUri(
301 "https://host-2/immutables-{immutable_file_number}".to_string(),
302 )),
303 compression_algorithm: None,
304 },
305 ],
306 },
307 ancillary: AncillaryMessagePart {
308 size_uncompressed: 4096,
309 locations: vec![AncillaryLocation::CloudStorage {
310 uri: "https://host-1/ancillary-3".to_string(),
311 compression_algorithm: Some(CompressionAlgorithm::Gzip),
312 }],
313 },
314 cardano_node_version: "0.0.1".to_string(),
315 }
316 }
317
318 #[test]
319 fn test_current_json_deserialized_into_current_message() {
320 let json = CURRENT_JSON;
321 let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
322 "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
323 );
324
325 assert_eq!(golden_current_message(), message);
326 }
327
328 #[test]
329 fn test_a_future_json_deserialized_with_unknown_location_types() {
330 let json = r#"
331 {
332 "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
333 "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
334 "network": "preview",
335 "beacon": {
336 "epoch": 123,
337 "immutable_file_number": 2345
338 },
339 "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
340 "total_db_size_uncompressed": 800796318,
341 "digests": {
342 "size_uncompressed": 1024,
343 "locations": [
344 {
345 "type": "whatever",
346 "new_field": "digest-1"
347 }
348 ]
349 },
350 "immutables": {
351 "average_size_uncompressed": 512,
352 "locations": [
353 {
354 "type": "whatever",
355 "new_field": [123, 125]
356 }
357 ]
358 },
359 "ancillary": {
360 "size_uncompressed": 4096,
361 "locations": [
362 {
363 "type": "whatever",
364 "new_field": "ancillary-3"
365 }
366 ]
367 },
368 "compression_algorithm": "gzip",
369 "cardano_node_version": "0.0.1",
370 "created_at": "2023-01-19T13:43:05.618857482Z"
371 }"#;
372 let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
373 "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
374 );
375
376 assert_eq!(message.digests.locations.len(), 1);
377 assert_eq!(DigestLocation::Unknown, message.digests.locations[0]);
378
379 assert_eq!(message.immutables.locations.len(), 1);
380 assert_eq!(ImmutablesLocation::Unknown, message.immutables.locations[0]);
381
382 assert_eq!(message.ancillary.locations.len(), 1);
383 assert_eq!(AncillaryLocation::Unknown, message.ancillary.locations[0]);
384 }
385
386 mod sanitize_immutable_locations {
387 use super::*;
388
389 #[test]
390 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
391 let immutable_locations = ImmutablesMessagePart {
392 locations: vec![ImmutablesLocation::CloudStorage {
393 uri: MultiFilesUri::Template(TemplateUri(
394 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
395 )),
396 compression_algorithm: None,
397 }],
398 average_size_uncompressed: 512,
399 };
400
401 let sanitize_locations = immutable_locations
402 .sanitized_locations()
403 .expect("Should succeed since there are no unknown locations.");
404 assert_eq!(sanitize_locations, immutable_locations.locations);
405 }
406
407 #[test]
408 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
409 let immutable_locations = ImmutablesMessagePart {
410 locations: vec![
411 ImmutablesLocation::CloudStorage {
412 uri: MultiFilesUri::Template(TemplateUri(
413 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
414 )),
415 compression_algorithm: None,
416 },
417 ImmutablesLocation::Unknown,
418 ],
419 average_size_uncompressed: 512,
420 };
421
422 let sanitize_locations = immutable_locations
423 .sanitized_locations()
424 .expect("Should succeed since not all locations are unknown.");
425 assert_eq!(
426 sanitize_locations,
427 vec![ImmutablesLocation::CloudStorage {
428 uri: MultiFilesUri::Template(TemplateUri(
429 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
430 )),
431 compression_algorithm: None,
432 }]
433 );
434 }
435
436 #[test]
437 fn fails_if_all_locations_are_unknown() {
438 ImmutablesMessagePart {
439 locations: vec![ImmutablesLocation::Unknown],
440 average_size_uncompressed: 512,
441 }
442 .sanitized_locations()
443 .expect_err("Should fail since all locations are unknown.");
444 }
445 }
446
447 mod sanitize_ancillary_locations {
448 use super::*;
449
450 #[test]
451 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
452 let ancillary_locations = AncillaryMessagePart {
453 locations: vec![AncillaryLocation::CloudStorage {
454 uri: "http://whatever/ancillary.tar.gz".to_string(),
455 compression_algorithm: None,
456 }],
457 size_uncompressed: 1024,
458 };
459
460 let sanitize_locations = ancillary_locations
461 .sanitized_locations()
462 .expect("Should succeed since there are no unknown locations.");
463 assert_eq!(sanitize_locations, ancillary_locations.locations);
464 }
465
466 #[test]
467 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
468 let ancillary_locations = AncillaryMessagePart {
469 locations: vec![
470 AncillaryLocation::CloudStorage {
471 uri: "http://whatever/digests.tar.gz".to_string(),
472 compression_algorithm: None,
473 },
474 AncillaryLocation::Unknown,
475 ],
476 size_uncompressed: 512,
477 };
478
479 let sanitize_locations = ancillary_locations
480 .sanitized_locations()
481 .expect("Should succeed since not all locations are unknown.");
482 assert_eq!(
483 sanitize_locations,
484 vec![AncillaryLocation::CloudStorage {
485 uri: "http://whatever/digests.tar.gz".to_string(),
486 compression_algorithm: None,
487 }]
488 );
489 }
490
491 #[test]
492 fn fails_if_all_locations_are_unknown() {
493 AncillaryMessagePart {
494 locations: vec![AncillaryLocation::Unknown],
495 size_uncompressed: 512,
496 }
497 .sanitized_locations()
498 .expect_err("Should fail since all locations are unknown.");
499 }
500 }
501
502 mod sanitize_digests_locations {
503 use super::*;
504
505 #[test]
506 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
507 let digests_locations = DigestsMessagePart {
508 locations: vec![DigestLocation::CloudStorage {
509 uri: "http://whatever/digests.tar.gz".to_string(),
510 compression_algorithm: None,
511 }],
512 size_uncompressed: 512,
513 };
514
515 let sanitize_locations = digests_locations
516 .sanitized_locations()
517 .expect("Should succeed since there are no unknown locations.");
518 assert_eq!(sanitize_locations, digests_locations.locations);
519 }
520
521 #[test]
522 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
523 let digests_locations = DigestsMessagePart {
524 locations: vec![
525 DigestLocation::CloudStorage {
526 uri: "http://whatever/digests.tar.gz".to_string(),
527 compression_algorithm: None,
528 },
529 DigestLocation::Unknown,
530 ],
531 size_uncompressed: 512,
532 };
533
534 let sanitize_locations = digests_locations
535 .sanitized_locations()
536 .expect("Should succeed since not all locations are unknown.");
537 assert_eq!(
538 sanitize_locations,
539 vec![DigestLocation::CloudStorage {
540 uri: "http://whatever/digests.tar.gz".to_string(),
541 compression_algorithm: None,
542 }]
543 );
544 }
545
546 #[test]
547 fn fails_if_all_locations_are_unknown() {
548 DigestsMessagePart {
549 locations: vec![DigestLocation::Unknown],
550 size_uncompressed: 512,
551 }
552 .sanitized_locations()
553 .expect_err("Should fail since all locations are unknown.");
554 }
555 }
556}