use std::path::{Path, PathBuf}; use crate::policy::SyncPreference; use crate::report::Warning; #[derive(Clone, Debug, PartialEq, Eq)] pub enum TalSource { Url(String), DerBytes { tal_url: String, tal_bytes: Vec, ta_der: Vec, }, FilePath(PathBuf), FilePathWithTa { tal_path: PathBuf, ta_path: PathBuf, }, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct TalInputSpec { pub tal_id: String, pub rir_id: String, pub source: TalSource, } impl TalInputSpec { pub fn from_url(url: impl Into) -> Self { let url = url.into(); let tal_id = derive_tal_id_from_url_like(&url); Self { rir_id: tal_id.clone(), tal_id, source: TalSource::Url(url), } } pub fn from_file_path(path: impl Into) -> Self { let path = path.into(); let tal_id = derive_tal_id_from_path(&path); Self { rir_id: tal_id.clone(), tal_id, source: TalSource::FilePath(path), } } pub fn from_file_path_with_ta( tal_path: impl Into, ta_path: impl Into, ) -> Self { let tal_path = tal_path.into(); let ta_path = ta_path.into(); let tal_id = derive_tal_id_from_path(&tal_path); Self { rir_id: tal_id.clone(), tal_id, source: TalSource::FilePathWithTa { tal_path, ta_path }, } } pub fn from_ta_der(tal_url: impl Into, tal_bytes: Vec, ta_der: Vec) -> Self { let tal_url = tal_url.into(); let tal_id = derive_tal_id_from_url_like(&tal_url); Self { rir_id: tal_id.clone(), tal_id, source: TalSource::DerBytes { tal_url, tal_bytes, ta_der, }, } } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RepoIdentity { pub notification_uri: Option, pub rsync_base_uri: String, } impl RepoIdentity { pub fn new(notification_uri: Option, rsync_base_uri: impl Into) -> Self { Self { notification_uri, rsync_base_uri: rsync_base_uri.into(), } } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum RepoDedupKey { RrdpNotify { notification_uri: String }, RsyncScope { rsync_scope_uri: String }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RepoTransportMode { Rrdp, Rsync, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoTransportTask { pub dedup_key: RepoDedupKey, pub repo_identity: RepoIdentity, pub mode: RepoTransportMode, pub tal_id: String, pub rir_id: String, pub validation_time: time::OffsetDateTime, pub priority: u8, pub requesters: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum RepoTransportResultKind { Success { source: String, warnings: Vec, }, Failed { detail: String, warnings: Vec, }, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoTransportResultEnvelope { pub dedup_key: RepoDedupKey, pub repo_identity: RepoIdentity, pub mode: RepoTransportMode, pub tal_id: String, pub rir_id: String, pub timing_ms: u64, pub result: RepoTransportResultKind, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RepoRuntimeState { Init, WaitingRrdp, RrdpOk, RrdpFailedPendingRsync, WaitingRsync, RsyncOk, FailedTerminal, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RepoKey { pub rsync_base_uri: String, pub notification_uri: Option, } impl RepoKey { pub fn new(rsync_base_uri: impl Into, notification_uri: Option) -> Self { Self { rsync_base_uri: rsync_base_uri.into(), notification_uri, } } pub fn as_identity(&self) -> RepoIdentity { RepoIdentity { notification_uri: self.notification_uri.clone(), rsync_base_uri: self.rsync_base_uri.clone(), } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoRequester { pub tal_id: String, pub rir_id: String, pub parent_node_id: Option, pub ca_instance_handle_id: String, pub publication_point_rsync_uri: String, pub manifest_rsync_uri: String, } impl RepoRequester { pub fn with_tal_rir( tal_id: impl Into, rir_id: impl Into, manifest_rsync_uri: impl Into, publication_point_rsync_uri: impl Into, ca_instance_handle_id: impl Into, ) -> Self { Self { tal_id: tal_id.into(), rir_id: rir_id.into(), parent_node_id: None, ca_instance_handle_id: ca_instance_handle_id.into(), publication_point_rsync_uri: publication_point_rsync_uri.into(), manifest_rsync_uri: manifest_rsync_uri.into(), } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoSyncTask { pub repo_key: RepoKey, pub validation_time: time::OffsetDateTime, pub sync_preference: SyncPreference, pub tal_id: String, pub rir_id: String, pub priority: u8, pub requesters: Vec, } impl RepoSyncTask { pub fn as_transport_task( &self, dedup_key: RepoDedupKey, mode: RepoTransportMode, ) -> RepoTransportTask { RepoTransportTask { dedup_key, repo_identity: self.repo_key.as_identity(), mode, tal_id: self.tal_id.clone(), rir_id: self.rir_id.clone(), validation_time: self.validation_time, priority: self.priority, requesters: self.requesters.clone(), } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RepoTaskState { Pending, Running, Succeeded, Failed, Reused, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoSyncResultRef { pub repo_key: RepoKey, pub source: String, } impl RepoSyncResultRef { pub fn as_identity(&self) -> RepoIdentity { self.repo_key.as_identity() } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct InFlightRepoEntry { pub state: RepoTaskState, pub task_ref: Option, pub waiting_requesters: Vec, pub result_ref: Option, pub last_result: Option, pub last_error: Option, pub started_at: Option, pub finished_at: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepoSyncResultEnvelope { pub repo_key: RepoKey, pub tal_id: String, pub rir_id: String, pub result: RepoSyncResultKind, pub phase: Option, pub timing_ms: u64, pub warnings: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum RepoSyncResultKind { Success(RepoSyncResultRef), Failed { detail: String }, Reused(RepoSyncResultRef), } fn derive_tal_id_from_url_like(s: &str) -> String { if let Ok(url) = url::Url::parse(s) { if let Some(last) = url .path_segments() .and_then(|segments| segments.filter(|seg| !seg.is_empty()).next_back()) { let stem = last.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(last); let trimmed = stem.trim(); if !trimmed.is_empty() { return trimmed.to_string(); } } if let Some(host) = url.host_str() { return host.to_string(); } } "unknown-tal".to_string() } fn derive_tal_id_from_path(path: &Path) -> String { path.file_stem() .and_then(|stem| stem.to_str()) .map(|s| s.trim()) .filter(|s| !s.is_empty()) .unwrap_or("unknown-tal") .to_string() } #[cfg(test)] mod tests { use std::path::Path; use crate::policy::SyncPreference; use crate::report::Warning; use super::{ RepoDedupKey, RepoIdentity, RepoKey, RepoRequester, RepoRuntimeState, RepoSyncTask, RepoTaskState, RepoTransportMode, RepoTransportResultEnvelope, RepoTransportResultKind, TalInputSpec, TalSource, derive_tal_id_from_path, derive_tal_id_from_url_like, }; #[test] fn tal_input_spec_from_url_derives_tal_and_rir_ids() { let spec = TalInputSpec::from_url("https://example.test/tals/apnic.tal"); assert_eq!(spec.tal_id, "apnic"); assert_eq!(spec.rir_id, "apnic"); assert_eq!( spec.source, TalSource::Url("https://example.test/tals/apnic.tal".to_string()) ); } #[test] fn tal_input_spec_from_file_path_derives_file_stem() { let spec = TalInputSpec::from_file_path("local/arin.tal"); assert_eq!(spec.tal_id, "arin"); assert_eq!(spec.rir_id, "arin"); } #[test] fn tal_input_spec_from_ta_der_preserves_payload() { let spec = TalInputSpec::from_ta_der( "https://example.test/ripe.tal", vec![4, 5, 6], vec![1, 2, 3], ); assert_eq!(spec.tal_id, "ripe"); assert_eq!(spec.rir_id, "ripe"); assert_eq!( spec.source, TalSource::DerBytes { tal_url: "https://example.test/ripe.tal".to_string(), tal_bytes: vec![4, 5, 6], ta_der: vec![1, 2, 3], } ); } #[test] fn repo_key_equality_uses_rsync_base_and_notification() { let a = RepoKey::new( "rsync://example.test/repo/", Some("https://example.test/notify.xml".to_string()), ); let b = RepoKey::new( "rsync://example.test/repo/", Some("https://example.test/notify.xml".to_string()), ); let c = RepoKey::new("rsync://example.test/repo/", None); assert_eq!(a, b); assert_ne!(a, c); } #[test] fn repo_task_state_variants_are_distinct() { assert_ne!(RepoTaskState::Pending, RepoTaskState::Running); assert_ne!(RepoTaskState::Succeeded, RepoTaskState::Failed); assert_ne!(RepoTaskState::Failed, RepoTaskState::Reused); } #[test] fn repo_identity_preserves_raw_inputs() { let ident = RepoIdentity::new( Some("https://example.test/notify.xml".to_string()), "rsync://example.test/repo/", ); assert_eq!( ident.notification_uri.as_deref(), Some("https://example.test/notify.xml") ); assert_eq!(ident.rsync_base_uri, "rsync://example.test/repo/"); } #[test] fn repo_key_can_be_viewed_as_repo_identity() { let key = RepoKey::new( "rsync://example.test/repo/", Some("https://example.test/notify.xml".to_string()), ); let ident = key.as_identity(); assert_eq!(ident.rsync_base_uri, "rsync://example.test/repo/"); assert_eq!( ident.notification_uri.as_deref(), Some("https://example.test/notify.xml") ); } #[test] fn repo_sync_task_maps_to_rrdp_transport_task() { let task = RepoSyncTask { repo_key: RepoKey::new( "rsync://example.test/repo/", Some("https://example.test/notify.xml".to_string()), ), validation_time: time::OffsetDateTime::UNIX_EPOCH, sync_preference: SyncPreference::RrdpThenRsync, tal_id: "apnic".to_string(), rir_id: "apnic".to_string(), priority: 1, requesters: vec![RepoRequester::with_tal_rir( "apnic", "apnic", "rsync://example.test/repo/root.mft", "rsync://example.test/repo/", "node:1", )], }; let transport = task.as_transport_task( RepoDedupKey::RrdpNotify { notification_uri: "https://example.test/notify.xml".to_string(), }, RepoTransportMode::Rrdp, ); assert_eq!(transport.mode, RepoTransportMode::Rrdp); assert_eq!(transport.tal_id, "apnic"); assert_eq!(transport.rir_id, "apnic"); assert_eq!(transport.requesters.len(), 1); assert_eq!( transport.repo_identity.notification_uri.as_deref(), Some("https://example.test/notify.xml") ); } #[test] fn repo_transport_result_envelope_supports_success_and_failure_shapes() { let identity = RepoIdentity::new(None, "rsync://example.test/repo/"); let ok = RepoTransportResultEnvelope { dedup_key: RepoDedupKey::RsyncScope { rsync_scope_uri: "rsync://example.test/module/".to_string(), }, repo_identity: identity.clone(), mode: RepoTransportMode::Rsync, tal_id: "arin".to_string(), rir_id: "arin".to_string(), timing_ms: 12, result: RepoTransportResultKind::Success { source: "rsync".to_string(), warnings: vec![Warning::new("ok")], }, }; let fail = RepoTransportResultEnvelope { dedup_key: RepoDedupKey::RsyncScope { rsync_scope_uri: "rsync://example.test/module/".to_string(), }, repo_identity: identity, mode: RepoTransportMode::Rsync, tal_id: "arin".to_string(), rir_id: "arin".to_string(), timing_ms: 30, result: RepoTransportResultKind::Failed { detail: "timeout".to_string(), warnings: vec![Warning::new("timeout")], }, }; assert!(matches!(ok.result, RepoTransportResultKind::Success { .. })); assert!(matches!( fail.result, RepoTransportResultKind::Failed { .. } )); } #[test] fn repo_runtime_state_variants_are_distinct() { assert_ne!(RepoRuntimeState::Init, RepoRuntimeState::WaitingRrdp); assert_ne!(RepoRuntimeState::RrdpOk, RepoRuntimeState::RsyncOk); assert_ne!( RepoRuntimeState::RrdpFailedPendingRsync, RepoRuntimeState::FailedTerminal ); } #[test] fn derive_tal_id_helpers_fall_back_safely() { assert_eq!( derive_tal_id_from_url_like("https://example.test/path/afrinic.tal"), "afrinic" ); assert_eq!( derive_tal_id_from_path(Path::new("foo/lacnic.tal")), "lacnic" ); } }