1use anyhow::anyhow;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::StdResult;
6use crate::entities::{
7 AncillaryLocation, AncillaryLocations, CardanoDbBeacon, DigestLocation, DigestsLocations,
8 ImmutablesLocation, ImmutablesLocations,
9};
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
159#[cfg(test)]
160mod tests {
161 use crate::entities::{CompressionAlgorithm, Epoch, MultiFilesUri, TemplateUri};
162
163 use super::*;
164
165 const CURRENT_JSON: &str = r#"
166 {
167 "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
168 "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
169 "network": "preview",
170 "beacon": {
171 "epoch": 123,
172 "immutable_file_number": 2345
173 },
174 "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
175 "total_db_size_uncompressed": 800796318,
176 "digests": {
177 "size_uncompressed": 1024,
178 "locations": [
179 {
180 "type": "aggregator",
181 "uri": "https://host-1/digest-1"
182 }
183 ]
184 },
185 "immutables": {
186 "average_size_uncompressed": 2048,
187 "locations": [
188 {
189 "type": "cloud_storage",
190 "uri": {
191 "Template": "https://host-1/immutables-{immutable_file_number}"
192 },
193 "compression_algorithm": "gzip"
194 },
195 {
196 "type": "cloud_storage",
197 "uri": {
198 "Template": "https://host-2/immutables-{immutable_file_number}"
199 }
200 }
201 ]
202 },
203 "ancillary": {
204 "size_uncompressed": 4096,
205 "locations": [
206 {
207 "type": "cloud_storage",
208 "uri": "https://host-1/ancillary-3",
209 "compression_algorithm": "gzip"
210 }
211 ]
212 },
213 "cardano_node_version": "0.0.1",
214 "created_at": "2023-01-19T13:43:05.618857482Z"
215 }"#;
216
217 fn golden_current_message() -> CardanoDatabaseSnapshotMessage {
218 CardanoDatabaseSnapshotMessage {
219 hash: "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb".to_string(),
220 merkle_root: "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6"
221 .to_string(),
222 network: "preview".to_string(),
223 beacon: CardanoDbBeacon {
224 epoch: Epoch(123),
225 immutable_file_number: 2345,
226 },
227 certificate_hash: "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb"
228 .to_string(),
229 total_db_size_uncompressed: 800796318,
230 created_at: DateTime::parse_from_rfc3339("2023-01-19T13:43:05.618857482Z")
231 .unwrap()
232 .with_timezone(&Utc),
233 digests: DigestsMessagePart {
234 size_uncompressed: 1024,
235 locations: vec![DigestLocation::Aggregator {
236 uri: "https://host-1/digest-1".to_string(),
237 }],
238 },
239 immutables: ImmutablesMessagePart {
240 average_size_uncompressed: 2048,
241 locations: vec![
242 ImmutablesLocation::CloudStorage {
243 uri: MultiFilesUri::Template(TemplateUri(
244 "https://host-1/immutables-{immutable_file_number}".to_string(),
245 )),
246 compression_algorithm: Some(CompressionAlgorithm::Gzip),
247 },
248 ImmutablesLocation::CloudStorage {
249 uri: MultiFilesUri::Template(TemplateUri(
250 "https://host-2/immutables-{immutable_file_number}".to_string(),
251 )),
252 compression_algorithm: None,
253 },
254 ],
255 },
256 ancillary: AncillaryMessagePart {
257 size_uncompressed: 4096,
258 locations: vec![AncillaryLocation::CloudStorage {
259 uri: "https://host-1/ancillary-3".to_string(),
260 compression_algorithm: Some(CompressionAlgorithm::Gzip),
261 }],
262 },
263 cardano_node_version: "0.0.1".to_string(),
264 }
265 }
266
267 #[test]
268 fn test_current_json_deserialized_into_current_message() {
269 let json = CURRENT_JSON;
270 let message: CardanoDatabaseSnapshotMessage = serde_json::from_str(json).expect(
271 "This JSON is expected to be successfully parsed into a CardanoDatabaseSnapshotMessage instance.",
272 );
273
274 assert_eq!(golden_current_message(), message);
275 }
276
277 #[test]
278 fn test_a_future_json_deserialized_with_unknown_location_types() {
279 let json = r#"
280 {
281 "hash": "d4071d518a3ace0f6c04a9c0745b9e9560e3e2af1b373bafc4e0398423e9abfb",
282 "merkle_root": "c8224920b9f5ad7377594eb8a15f34f08eb3103cc5241d57cafc5638403ec7c6",
283 "network": "preview",
284 "beacon": {
285 "epoch": 123,
286 "immutable_file_number": 2345
287 },
288 "certificate_hash": "f6c01b373bafc4e039844071d5da3ace4a9c0745b9e9560e3e2af01823e9abfb",
289 "total_db_size_uncompressed": 800796318,
290 "digests": {
291 "size_uncompressed": 1024,
292 "locations": [
293 {
294 "type": "whatever",
295 "new_field": "digest-1"
296 }
297 ]
298 },
299 "immutables": {
300 "average_size_uncompressed": 512,
301 "locations": [
302 {
303 "type": "whatever",
304 "new_field": [123, 125]
305 }
306 ]
307 },
308 "ancillary": {
309 "size_uncompressed": 4096,
310 "locations": [
311 {
312 "type": "whatever",
313 "new_field": "ancillary-3"
314 }
315 ]
316 },
317 "compression_algorithm": "gzip",
318 "cardano_node_version": "0.0.1",
319 "created_at": "2023-01-19T13:43:05.618857482Z"
320 }"#;
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!(message.digests.locations.len(), 1);
326 assert_eq!(DigestLocation::Unknown, message.digests.locations[0]);
327
328 assert_eq!(message.immutables.locations.len(), 1);
329 assert_eq!(ImmutablesLocation::Unknown, message.immutables.locations[0]);
330
331 assert_eq!(message.ancillary.locations.len(), 1);
332 assert_eq!(AncillaryLocation::Unknown, message.ancillary.locations[0]);
333 }
334
335 mod sanitize_immutable_locations {
336 use super::*;
337
338 #[test]
339 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
340 let immutable_locations = ImmutablesMessagePart {
341 locations: vec![ImmutablesLocation::CloudStorage {
342 uri: MultiFilesUri::Template(TemplateUri(
343 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
344 )),
345 compression_algorithm: None,
346 }],
347 average_size_uncompressed: 512,
348 };
349
350 let sanitize_locations = immutable_locations
351 .sanitized_locations()
352 .expect("Should succeed since there are no unknown locations.");
353 assert_eq!(sanitize_locations, immutable_locations.locations);
354 }
355
356 #[test]
357 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
358 let immutable_locations = ImmutablesMessagePart {
359 locations: vec![
360 ImmutablesLocation::CloudStorage {
361 uri: MultiFilesUri::Template(TemplateUri(
362 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
363 )),
364 compression_algorithm: None,
365 },
366 ImmutablesLocation::Unknown,
367 ],
368 average_size_uncompressed: 512,
369 };
370
371 let sanitize_locations = immutable_locations
372 .sanitized_locations()
373 .expect("Should succeed since not all locations are unknown.");
374 assert_eq!(
375 sanitize_locations,
376 vec![ImmutablesLocation::CloudStorage {
377 uri: MultiFilesUri::Template(TemplateUri(
378 "http://whatever/{immutable_file_number}.tar.gz".to_string(),
379 )),
380 compression_algorithm: None,
381 }]
382 );
383 }
384
385 #[test]
386 fn fails_if_all_locations_are_unknown() {
387 ImmutablesMessagePart {
388 locations: vec![ImmutablesLocation::Unknown],
389 average_size_uncompressed: 512,
390 }
391 .sanitized_locations()
392 .expect_err("Should fail since all locations are unknown.");
393 }
394 }
395
396 mod sanitize_ancillary_locations {
397 use super::*;
398
399 #[test]
400 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
401 let ancillary_locations = AncillaryMessagePart {
402 locations: vec![AncillaryLocation::CloudStorage {
403 uri: "http://whatever/ancillary.tar.gz".to_string(),
404 compression_algorithm: None,
405 }],
406 size_uncompressed: 1024,
407 };
408
409 let sanitize_locations = ancillary_locations
410 .sanitized_locations()
411 .expect("Should succeed since there are no unknown locations.");
412 assert_eq!(sanitize_locations, ancillary_locations.locations);
413 }
414
415 #[test]
416 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
417 let ancillary_locations = AncillaryMessagePart {
418 locations: vec![
419 AncillaryLocation::CloudStorage {
420 uri: "http://whatever/digests.tar.gz".to_string(),
421 compression_algorithm: None,
422 },
423 AncillaryLocation::Unknown,
424 ],
425 size_uncompressed: 512,
426 };
427
428 let sanitize_locations = ancillary_locations
429 .sanitized_locations()
430 .expect("Should succeed since not all locations are unknown.");
431 assert_eq!(
432 sanitize_locations,
433 vec![AncillaryLocation::CloudStorage {
434 uri: "http://whatever/digests.tar.gz".to_string(),
435 compression_algorithm: None,
436 }]
437 );
438 }
439
440 #[test]
441 fn fails_if_all_locations_are_unknown() {
442 AncillaryMessagePart {
443 locations: vec![AncillaryLocation::Unknown],
444 size_uncompressed: 512,
445 }
446 .sanitized_locations()
447 .expect_err("Should fail since all locations are unknown.");
448 }
449 }
450
451 mod sanitize_digests_locations {
452 use super::*;
453
454 #[test]
455 fn succeeds_and_leave_all_locations_intact_if_no_unknown_location() {
456 let digests_locations = DigestsMessagePart {
457 locations: vec![DigestLocation::CloudStorage {
458 uri: "http://whatever/digests.tar.gz".to_string(),
459 compression_algorithm: None,
460 }],
461 size_uncompressed: 512,
462 };
463
464 let sanitize_locations = digests_locations
465 .sanitized_locations()
466 .expect("Should succeed since there are no unknown locations.");
467 assert_eq!(sanitize_locations, digests_locations.locations);
468 }
469
470 #[test]
471 fn succeeds_and_remove_unknown_locations_if_some_locations_are_not_unknown() {
472 let digests_locations = DigestsMessagePart {
473 locations: vec![
474 DigestLocation::CloudStorage {
475 uri: "http://whatever/digests.tar.gz".to_string(),
476 compression_algorithm: None,
477 },
478 DigestLocation::Unknown,
479 ],
480 size_uncompressed: 512,
481 };
482
483 let sanitize_locations = digests_locations
484 .sanitized_locations()
485 .expect("Should succeed since not all locations are unknown.");
486 assert_eq!(
487 sanitize_locations,
488 vec![DigestLocation::CloudStorage {
489 uri: "http://whatever/digests.tar.gz".to_string(),
490 compression_algorithm: None,
491 }]
492 );
493 }
494
495 #[test]
496 fn fails_if_all_locations_are_unknown() {
497 DigestsMessagePart {
498 locations: vec![DigestLocation::Unknown],
499 size_uncompressed: 512,
500 }
501 .sanitized_locations()
502 .expect_err("Should fail since all locations are unknown.");
503 }
504 }
505}