1use std::borrow::Cow;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct EventAlias {
11 pub exchange: &'static str,
12 pub group_id: &'static str,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct EventAliasOwned {
17 pub exchange: String,
18 pub group_id: String,
19}
20
21#[derive(Debug, Clone, Copy)]
22struct CanonicalEventEntry {
23 canonical_event_id: &'static str,
24 aliases: &'static [EventAlias],
25}
26
27const CANONICAL_EVENT_REGISTRY: &[CanonicalEventEntry] = &[CanonicalEventEntry {
36 canonical_event_id: "ev:us-pres-election-winner-2028",
37 aliases: &[EventAlias {
38 exchange: "polymarket",
39 group_id: "31552",
40 }],
41}];
42
43fn normalize_exchange(exchange: &str) -> Cow<'_, str> {
44 let trimmed = exchange.trim();
45 if trimmed.bytes().all(|b| !b.is_ascii_uppercase()) {
46 Cow::Borrowed(trimmed)
47 } else {
48 Cow::Owned(trimmed.to_ascii_lowercase())
49 }
50}
51
52pub fn default_event_id(exchange: &str, group_id: &str) -> Option<String> {
56 let exchange = normalize_exchange(exchange);
57 let group_id = group_id.trim();
58 if exchange.is_empty() || group_id.is_empty() {
59 return None;
60 }
61 Some(format!("ev:{exchange}:{group_id}"))
62}
63
64pub fn canonical_event_id(exchange: &str, group_id: &str) -> Option<String> {
66 let exchange_norm = normalize_exchange(exchange);
67 let group_norm = group_id.trim();
68 if exchange_norm.is_empty() || group_norm.is_empty() {
69 return None;
70 }
71
72 for entry in CANONICAL_EVENT_REGISTRY {
73 if entry
74 .aliases
75 .iter()
76 .any(|alias| alias.exchange == exchange_norm && alias.group_id == group_norm)
77 {
78 return Some(entry.canonical_event_id.to_string());
79 }
80 }
81
82 default_event_id(&exchange_norm, group_norm)
83}
84
85pub fn aliases_for_event_id(event_id: &str) -> Vec<EventAliasOwned> {
91 let event_id = event_id.trim();
92 if event_id.is_empty() {
93 return Vec::new();
94 }
95
96 if let Some(entry) = CANONICAL_EVENT_REGISTRY
97 .iter()
98 .find(|entry| entry.canonical_event_id == event_id)
99 {
100 return entry
101 .aliases
102 .iter()
103 .map(|alias| EventAliasOwned {
104 exchange: alias.exchange.to_string(),
105 group_id: alias.group_id.to_string(),
106 })
107 .collect();
108 }
109
110 let raw = match event_id.strip_prefix("ev:") {
111 Some(v) => v,
112 None => return Vec::new(),
113 };
114
115 if let Some((exchange, group_id)) = raw.split_once(':') {
116 let exchange = normalize_exchange(exchange);
117 let group_id = group_id.trim();
118 if !exchange.is_empty() && !group_id.is_empty() {
119 return vec![EventAliasOwned {
120 exchange: exchange.into_owned(),
121 group_id: group_id.to_string(),
122 }];
123 }
124 }
125
126 Vec::new()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::{aliases_for_event_id, canonical_event_id, default_event_id};
132
133 #[test]
134 fn canonical_event_id_uses_registry() {
135 let id = canonical_event_id("polymarket", "31552");
136 assert_eq!(id.as_deref(), Some("ev:us-pres-election-winner-2028"));
137 }
138
139 #[test]
140 fn canonical_event_id_falls_back_deterministically() {
141 let id = canonical_event_id("kalshi", "KXABC-123");
142 assert_eq!(id.as_deref(), Some("ev:kalshi:KXABC-123"));
143 }
144
145 #[test]
146 fn aliases_expand_registry_and_fallback() {
147 let mapped = aliases_for_event_id("ev:us-pres-election-winner-2028");
148 assert_eq!(mapped.len(), 1);
149 assert_eq!(mapped[0].exchange, "polymarket");
150 assert_eq!(mapped[0].group_id, "31552");
151
152 let fallback = aliases_for_event_id("ev:kalshi:KXABC-123");
153 assert_eq!(fallback.len(), 1);
154 assert_eq!(fallback[0].exchange, "kalshi");
155 assert_eq!(fallback[0].group_id, "KXABC-123");
156 }
157
158 #[test]
159 fn default_event_id_validates_inputs() {
160 assert_eq!(
161 default_event_id("kalshi", "ABC").as_deref(),
162 Some("ev:kalshi:ABC")
163 );
164 assert!(default_event_id("", "ABC").is_none());
165 assert!(default_event_id("kalshi", "").is_none());
166 }
167}