1use crate::error::BuildError;
7use crate::id_generator::{StableHashConfig, StableHashGenerator};
8use indexmap::{IndexMap, IndexSet};
9
10#[derive(Debug, Clone)]
28pub struct ReferenceConfig {
29 pub deterministic: bool,
31 pub resource_prefix: String,
33 pub release_prefix: String,
35 pub deal_prefix: String,
37 pub max_cache_size: usize,
39}
40
41impl Default for ReferenceConfig {
42 fn default() -> Self {
43 Self {
44 deterministic: true,
45 resource_prefix: "R".to_string(),
46 release_prefix: "REL".to_string(),
47 deal_prefix: "D".to_string(),
48 max_cache_size: 100_000, }
50 }
51}
52
53#[derive(Debug, Clone)]
72pub struct ResourceReference {
73 pub reference_id: String,
75 pub resource_id: String,
77 pub title: String,
79 pub artist: String,
81 pub resource_type: String,
83 pub sequence_number: usize,
85}
86
87#[derive(Debug, Clone)]
106pub struct ReleaseReference {
107 pub reference_id: String,
109 pub release_id: String,
111 pub title: String,
113 pub artist: String,
115 pub resource_references: Vec<String>,
117 pub sequence_number: usize,
119}
120
121pub struct StreamingReferenceManager {
123 config: ReferenceConfig,
124 #[allow(dead_code)]
125 hash_generator: StableHashGenerator,
126
127 resource_references: IndexMap<String, String>, resource_metadata: IndexMap<String, ResourceReference>,
130 resource_sequence: usize,
131
132 release_references: IndexMap<String, String>, release_metadata: IndexMap<String, ReleaseReference>,
135 release_sequence: usize,
136
137 deal_references: IndexMap<String, String>, deal_sequence: usize,
140
141 used_references: IndexSet<String>,
143 orphaned_references: Vec<String>,
144 duplicate_resource_ids: IndexSet<String>,
145 duplicate_release_ids: IndexSet<String>,
146
147 references_generated: usize,
149}
150
151impl StreamingReferenceManager {
152 pub fn new() -> Self {
154 Self::new_with_config(ReferenceConfig::default())
155 }
156
157 pub fn new_with_config(config: ReferenceConfig) -> Self {
159 let hash_config = StableHashConfig::default();
160
161 StreamingReferenceManager {
162 config,
163 hash_generator: StableHashGenerator::new(hash_config),
164 resource_references: IndexMap::new(),
165 resource_metadata: IndexMap::new(),
166 resource_sequence: 1,
167 release_references: IndexMap::new(),
168 release_metadata: IndexMap::new(),
169 release_sequence: 1,
170 deal_references: IndexMap::new(),
171 deal_sequence: 1,
172 used_references: IndexSet::new(),
173 orphaned_references: Vec::new(),
174 duplicate_resource_ids: IndexSet::new(),
175 duplicate_release_ids: IndexSet::new(),
176 references_generated: 0,
177 }
178 }
179
180 pub fn generate_resource_reference(&mut self, resource_id: &str) -> Result<String, BuildError> {
182 if self.resource_references.contains_key(resource_id) {
184 self.duplicate_resource_ids.insert(resource_id.to_string());
185 return Ok(self.resource_references[resource_id].clone());
186 }
187
188 let reference_id = if self.config.deterministic {
190 use sha2::{Digest, Sha256};
192 let mut hasher = Sha256::new();
193 hasher.update(resource_id.as_bytes());
194 let hash = format!("{:x}", hasher.finalize());
195 format!("{}{}", self.config.resource_prefix, &hash[..8])
196 } else {
197 format!(
198 "{}{:06}",
199 self.config.resource_prefix, self.resource_sequence
200 )
201 };
202
203 if self.used_references.contains(&reference_id) {
205 return Err(BuildError::InvalidReference {
206 reference: reference_id,
207 });
208 }
209
210 self.resource_references
212 .insert(resource_id.to_string(), reference_id.clone());
213 self.used_references.insert(reference_id.clone());
214 self.resource_sequence += 1;
215 self.references_generated += 1;
216
217 self.manage_cache_size()?;
219
220 Ok(reference_id)
221 }
222
223 pub fn generate_release_reference(&mut self, release_id: &str) -> Result<String, BuildError> {
225 if self.release_references.contains_key(release_id) {
227 self.duplicate_release_ids.insert(release_id.to_string());
228 return Ok(self.release_references[release_id].clone());
229 }
230
231 let reference_id = if self.config.deterministic {
233 use sha2::{Digest, Sha256};
235 let mut hasher = Sha256::new();
236 hasher.update(release_id.as_bytes());
237 let hash = format!("{:x}", hasher.finalize());
238 format!("{}{}", self.config.release_prefix, &hash[..8])
239 } else {
240 format!("{}{:06}", self.config.release_prefix, self.release_sequence)
241 };
242
243 if self.used_references.contains(&reference_id) {
245 return Err(BuildError::InvalidReference {
246 reference: reference_id,
247 });
248 }
249
250 self.release_references
252 .insert(release_id.to_string(), reference_id.clone());
253 self.used_references.insert(reference_id.clone());
254 self.release_sequence += 1;
255 self.references_generated += 1;
256
257 self.manage_cache_size()?;
259
260 Ok(reference_id)
261 }
262
263 pub fn generate_deal_reference(&mut self, deal_id: &str) -> Result<String, BuildError> {
265 if let Some(existing_ref) = self.deal_references.get(deal_id) {
267 return Ok(existing_ref.clone());
268 }
269
270 let reference_id = if self.config.deterministic {
272 use sha2::{Digest, Sha256};
274 let mut hasher = Sha256::new();
275 hasher.update(deal_id.as_bytes());
276 let hash = format!("{:x}", hasher.finalize());
277 format!("{}{}", self.config.deal_prefix, &hash[..8])
278 } else {
279 format!("{}{:06}", self.config.deal_prefix, self.deal_sequence)
280 };
281
282 if self.used_references.contains(&reference_id) {
284 return Err(BuildError::InvalidReference {
285 reference: reference_id,
286 });
287 }
288
289 self.deal_references
291 .insert(deal_id.to_string(), reference_id.clone());
292 self.used_references.insert(reference_id.clone());
293 self.deal_sequence += 1;
294 self.references_generated += 1;
295
296 self.manage_cache_size()?;
298
299 Ok(reference_id)
300 }
301
302 pub fn store_resource_metadata(
304 &mut self,
305 resource_id: &str,
306 title: &str,
307 artist: &str,
308 resource_type: &str,
309 ) -> Result<(), BuildError> {
310 let reference_id = self
311 .resource_references
312 .get(resource_id)
313 .ok_or_else(|| BuildError::InvalidReference {
314 reference: format!("Resource {} not found", resource_id),
315 })?
316 .clone();
317
318 let metadata = ResourceReference {
319 reference_id: reference_id.clone(),
320 resource_id: resource_id.to_string(),
321 title: title.to_string(),
322 artist: artist.to_string(),
323 resource_type: resource_type.to_string(),
324 sequence_number: self.resource_metadata.len() + 1,
325 };
326
327 self.resource_metadata.insert(reference_id, metadata);
328 Ok(())
329 }
330
331 pub fn store_release_metadata(
333 &mut self,
334 release_id: &str,
335 title: &str,
336 artist: &str,
337 resource_references: Vec<String>,
338 ) -> Result<(), BuildError> {
339 let reference_id = self
340 .release_references
341 .get(release_id)
342 .ok_or_else(|| BuildError::InvalidReference {
343 reference: format!("Release {} not found", release_id),
344 })?
345 .clone();
346
347 for resource_ref in &resource_references {
349 if !self.used_references.contains(resource_ref) {
350 self.orphaned_references.push(resource_ref.clone());
351 }
352 }
353
354 let metadata = ReleaseReference {
355 reference_id: reference_id.clone(),
356 release_id: release_id.to_string(),
357 title: title.to_string(),
358 artist: artist.to_string(),
359 resource_references,
360 sequence_number: self.release_metadata.len() + 1,
361 };
362
363 self.release_metadata.insert(reference_id, metadata);
364 Ok(())
365 }
366
367 pub fn validate_references(&self) -> ReferenceValidationResult {
369 let mut errors = Vec::new();
370 let mut warnings = Vec::new();
371
372 if !self.orphaned_references.is_empty() {
374 warnings.push(format!(
375 "Found {} orphaned resource references",
376 self.orphaned_references.len()
377 ));
378 }
379
380 if !self.duplicate_resource_ids.is_empty() {
382 warnings.push(format!(
383 "Found {} duplicate resource IDs",
384 self.duplicate_resource_ids.len()
385 ));
386 }
387
388 if !self.duplicate_release_ids.is_empty() {
390 warnings.push(format!(
391 "Found {} duplicate release IDs",
392 self.duplicate_release_ids.len()
393 ));
394 }
395
396 for (resource_id, reference_id) in &self.resource_references {
398 if !self.used_references.contains(reference_id) {
399 errors.push(format!(
400 "Resource reference {} for resource {} not properly tracked",
401 reference_id, resource_id
402 ));
403 }
404 }
405
406 for (release_id, reference_id) in &self.release_references {
407 if !self.used_references.contains(reference_id) {
408 errors.push(format!(
409 "Release reference {} for release {} not properly tracked",
410 reference_id, release_id
411 ));
412 }
413 }
414
415 ReferenceValidationResult {
416 is_valid: errors.is_empty(),
417 errors,
418 warnings,
419 total_references: self.references_generated,
420 resource_count: self.resource_references.len(),
421 release_count: self.release_references.len(),
422 deal_count: self.deal_references.len(),
423 }
424 }
425
426 pub fn get_stats(&self) -> ReferenceStats {
428 ReferenceStats {
429 total_references_generated: self.references_generated,
430 resource_references: self.resource_references.len(),
431 release_references: self.release_references.len(),
432 deal_references: self.deal_references.len(),
433 cache_size: self.current_cache_size(),
434 duplicate_resource_ids: self.duplicate_resource_ids.len(),
435 duplicate_release_ids: self.duplicate_release_ids.len(),
436 orphaned_references: self.orphaned_references.len(),
437 }
438 }
439
440 pub fn get_resource_reference(&self, resource_id: &str) -> Option<&str> {
442 self.resource_references
443 .get(resource_id)
444 .map(|s| s.as_str())
445 }
446
447 pub fn get_release_reference(&self, release_id: &str) -> Option<&str> {
449 self.release_references.get(release_id).map(|s| s.as_str())
450 }
451
452 fn manage_cache_size(&mut self) -> Result<(), BuildError> {
454 let current_size = self.current_cache_size();
455
456 if current_size > self.config.max_cache_size {
457 let to_remove = current_size / 4;
459
460 let resource_to_remove =
462 std::cmp::min(to_remove / 2, self.resource_references.len() / 2);
463 for _ in 0..resource_to_remove {
464 if let Some((_resource_id, reference_id)) =
465 self.resource_references.shift_remove_index(0)
466 {
467 self.resource_metadata.shift_remove(&reference_id);
468 self.used_references.shift_remove(&reference_id);
469 }
470 }
471
472 let release_to_remove = std::cmp::min(to_remove / 2, self.release_references.len() / 2);
474 for _ in 0..release_to_remove {
475 if let Some((_release_id, reference_id)) =
476 self.release_references.shift_remove_index(0)
477 {
478 self.release_metadata.shift_remove(&reference_id);
479 self.used_references.shift_remove(&reference_id);
480 }
481 }
482 }
483
484 Ok(())
485 }
486
487 fn current_cache_size(&self) -> usize {
488 self.resource_references.len()
489 + self.release_references.len()
490 + self.deal_references.len()
491 + self.resource_metadata.len()
492 + self.release_metadata.len()
493 }
494}
495
496impl Default for StreamingReferenceManager {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502#[derive(Debug)]
532pub struct ReferenceValidation {
533 pub is_valid: bool,
535 pub errors: Vec<String>,
537 pub warnings: Vec<String>,
539 pub total_references: usize,
541 pub resource_count: usize,
543 pub release_count: usize,
545 pub deal_count: usize,
547}
548
549#[derive(Debug, Clone)]
551pub struct ReferenceValidationResult {
552 pub is_valid: bool,
554 pub errors: Vec<String>,
556 pub warnings: Vec<String>,
558 pub total_references: usize,
560 pub resource_count: usize,
562 pub release_count: usize,
564 pub deal_count: usize,
566}
567
568#[derive(Debug, Default)]
588pub struct ReferenceStats {
589 pub total_references_generated: usize,
591 pub resource_references: usize,
593 pub release_references: usize,
595 pub deal_references: usize,
597 pub cache_size: usize,
599 pub duplicate_resource_ids: usize,
601 pub duplicate_release_ids: usize,
603 pub orphaned_references: usize,
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_resource_reference_generation() {
613 let mut manager = StreamingReferenceManager::new();
614
615 let ref1 = manager.generate_resource_reference("resource1").unwrap();
616 let ref2 = manager.generate_resource_reference("resource2").unwrap();
617
618 assert_ne!(ref1, ref2);
619 assert!(ref1.starts_with("R"));
620 assert!(ref2.starts_with("R"));
621
622 let ref3 = manager.generate_resource_reference("resource1").unwrap();
624 assert_eq!(ref1, ref3);
625 }
626
627 #[test]
628 fn test_release_reference_generation() {
629 let mut manager = StreamingReferenceManager::new();
630
631 let ref1 = manager.generate_release_reference("release1").unwrap();
632 let ref2 = manager.generate_release_reference("release2").unwrap();
633
634 assert_ne!(ref1, ref2);
635 assert!(ref1.starts_with("REL"));
636 assert!(ref2.starts_with("REL"));
637 }
638
639 #[test]
640 fn test_metadata_storage() {
641 let mut manager = StreamingReferenceManager::new();
642
643 let resource_ref = manager.generate_resource_reference("resource1").unwrap();
644 manager
645 .store_resource_metadata("resource1", "Title", "Artist", "SoundRecording")
646 .unwrap();
647
648 let metadata = manager.resource_metadata.get(&resource_ref).unwrap();
649 assert_eq!(metadata.title, "Title");
650 assert_eq!(metadata.artist, "Artist");
651 }
652
653 #[test]
654 fn test_reference_validation() {
655 let mut manager = StreamingReferenceManager::new();
656
657 let resource_ref = manager.generate_resource_reference("resource1").unwrap();
659 let _release_ref = manager.generate_release_reference("release1").unwrap();
660
661 manager
663 .store_release_metadata("release1", "Album Title", "Artist", vec![resource_ref])
664 .unwrap();
665
666 let validation = manager.validate_references();
667 assert!(validation.is_valid);
668 assert_eq!(validation.resource_count, 1);
669 assert_eq!(validation.release_count, 1);
670 }
671
672 #[test]
673 fn test_orphaned_references() {
674 let mut manager = StreamingReferenceManager::new();
675
676 let _release_ref = manager.generate_release_reference("release1").unwrap();
677
678 manager
680 .store_release_metadata(
681 "release1",
682 "Album Title",
683 "Artist",
684 vec!["R999999".to_string()],
685 )
686 .unwrap();
687
688 let validation = manager.validate_references();
689 assert!(!validation.warnings.is_empty());
690 }
691}