tesser_core/
identifiers.rs

1use once_cell::sync::Lazy;
2use parking_lot::RwLock;
3use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
4use std::collections::HashMap;
5use std::fmt;
6use std::hash::Hash;
7use std::str::FromStr;
8
9const UNSPECIFIED_EXCHANGE_ID: u16 = 0;
10
11static EXCHANGES: Lazy<RwLock<ExchangeRegistry>> = Lazy::new(|| {
12    RwLock::new(ExchangeRegistry {
13        next_id: 1,
14        ..ExchangeRegistry::default()
15    })
16});
17
18static ASSETS: Lazy<RwLock<AssetRegistry>> = Lazy::new(|| RwLock::new(AssetRegistry::default()));
19static SYMBOLS: Lazy<RwLock<SymbolRegistry>> = Lazy::new(|| RwLock::new(SymbolRegistry::default()));
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct ExchangeId(u16);
23
24impl ExchangeId {
25    pub const UNSPECIFIED: Self = Self(UNSPECIFIED_EXCHANGE_ID);
26
27    #[must_use]
28    pub const fn from_raw(value: u16) -> Self {
29        Self(value)
30    }
31
32    #[must_use]
33    pub const fn as_raw(self) -> u16 {
34        self.0
35    }
36
37    #[must_use]
38    pub fn is_specified(self) -> bool {
39        self.0 != UNSPECIFIED_EXCHANGE_ID
40    }
41
42    pub fn register(name: impl AsRef<str>) -> Self {
43        let name = canonicalize(name.as_ref());
44        if name.is_empty() {
45            return Self::UNSPECIFIED;
46        }
47        let mut registry = EXCHANGES.write();
48        if let Some(id) = registry.name_to_id.get(&name) {
49            return *id;
50        }
51        let id = ExchangeId(registry.next_id);
52        registry.next_id = registry.next_id.saturating_add(1);
53        let stored = leak_string(name.clone());
54        registry.id_to_name.insert(id, stored);
55        registry.name_to_id.insert(name, id);
56        id
57    }
58
59    #[must_use]
60    pub fn name(self) -> &'static str {
61        if self == Self::UNSPECIFIED {
62            return "unspecified";
63        }
64        let registry = EXCHANGES.read();
65        registry
66            .id_to_name
67            .get(&self)
68            .copied()
69            .unwrap_or_else(|| leak_string(format!("exchange#{}", self.0)))
70    }
71}
72
73impl Default for ExchangeId {
74    fn default() -> Self {
75        Self::UNSPECIFIED
76    }
77}
78
79impl fmt::Display for ExchangeId {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.write_str(self.name())
82    }
83}
84
85impl FromStr for ExchangeId {
86    type Err = IdentifierParseError;
87
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        let name = canonicalize(s);
90        if name.is_empty() {
91            return Err(IdentifierParseError::new("exchange", s));
92        }
93        let mut registry = EXCHANGES.write();
94        if let Some(id) = registry.name_to_id.get(&name) {
95            return Ok(*id);
96        }
97        let id = ExchangeId(registry.next_id);
98        registry.next_id = registry.next_id.saturating_add(1);
99        let stored = leak_string(name.clone());
100        registry.id_to_name.insert(id, stored);
101        registry.name_to_id.insert(name, id);
102        Ok(id)
103    }
104}
105
106impl Serialize for ExchangeId {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        serializer.serialize_str(self.as_ref())
112    }
113}
114
115impl<'de> Deserialize<'de> for ExchangeId {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: Deserializer<'de>,
119    {
120        let raw = String::deserialize(deserializer)?;
121        raw.parse().map_err(D::Error::custom)
122    }
123}
124
125impl From<&str> for ExchangeId {
126    fn from(value: &str) -> Self {
127        value.parse().unwrap_or(Self::UNSPECIFIED)
128    }
129}
130
131impl From<String> for ExchangeId {
132    fn from(value: String) -> Self {
133        value.parse().unwrap_or(Self::UNSPECIFIED)
134    }
135}
136
137impl From<&ExchangeId> for ExchangeId {
138    fn from(value: &ExchangeId) -> Self {
139        *value
140    }
141}
142
143impl AsRef<str> for ExchangeId {
144    fn as_ref(&self) -> &str {
145        self.name()
146    }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
150pub struct AssetId {
151    pub exchange: ExchangeId,
152    pub asset_id: u32,
153}
154
155impl AssetId {
156    pub const fn new(exchange: ExchangeId, asset_id: u32) -> Self {
157        Self { exchange, asset_id }
158    }
159
160    pub fn from_code(exchange: ExchangeId, code: impl AsRef<str>) -> Self {
161        let code = canonicalize_asset(code.as_ref());
162        if code.is_empty() {
163            return Self::unspecified();
164        }
165        let mut registry = ASSETS.write();
166        let key = (exchange, code.clone());
167        if let Some(existing) = registry.name_to_id.get(&key) {
168            return Self::new(exchange, *existing);
169        }
170        let next = registry
171            .next_per_exchange
172            .entry(exchange)
173            .and_modify(|id| *id = id.saturating_add(1))
174            .or_insert(1);
175        let id = *next;
176        registry.name_to_id.insert(key, id);
177        registry
178            .id_to_name
179            .insert((exchange, id), leak_string(code));
180        Self::new(exchange, id)
181    }
182
183    #[must_use]
184    pub fn code(&self) -> &'static str {
185        asset_code_lookup(self.exchange, self.asset_id)
186    }
187
188    pub const fn unspecified() -> Self {
189        Self::new(ExchangeId::UNSPECIFIED, 0)
190    }
191}
192
193impl Default for AssetId {
194    fn default() -> Self {
195        Self::unspecified()
196    }
197}
198
199impl fmt::Display for AssetId {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{}:{}", self.exchange, self.code())
202    }
203}
204
205impl FromStr for AssetId {
206    type Err = IdentifierParseError;
207
208    fn from_str(s: &str) -> Result<Self, Self::Err> {
209        let (exchange, code) = split_identifier(s, "asset")?;
210        Ok(Self::from_code(exchange, code))
211    }
212}
213
214impl Serialize for AssetId {
215    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
216    where
217        S: Serializer,
218    {
219        serializer.serialize_str(self.as_ref())
220    }
221}
222
223impl<'de> Deserialize<'de> for AssetId {
224    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225    where
226        D: Deserializer<'de>,
227    {
228        let value = String::deserialize(deserializer)?;
229        value.parse().map_err(D::Error::custom)
230    }
231}
232
233impl From<&str> for AssetId {
234    fn from(value: &str) -> Self {
235        value
236            .parse()
237            .unwrap_or_else(|_| Self::from_code(ExchangeId::UNSPECIFIED, value))
238    }
239}
240
241impl From<String> for AssetId {
242    fn from(value: String) -> Self {
243        AssetId::from(value.as_str())
244    }
245}
246
247impl From<&AssetId> for AssetId {
248    fn from(value: &AssetId) -> Self {
249        *value
250    }
251}
252
253impl AsRef<str> for AssetId {
254    fn as_ref(&self) -> &str {
255        self.code()
256    }
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
260pub struct Symbol {
261    pub exchange: ExchangeId,
262    pub market_id: u32,
263}
264
265impl Symbol {
266    pub const fn new(exchange: ExchangeId, market_id: u32) -> Self {
267        Self {
268            exchange,
269            market_id,
270        }
271    }
272
273    pub fn from_code(exchange: ExchangeId, code: impl AsRef<str>) -> Self {
274        let code = code.as_ref().trim();
275        if code.is_empty() {
276            return Self::unspecified();
277        }
278        let normalized = code.to_uppercase();
279        let mut registry = SYMBOLS.write();
280        let key = (exchange, normalized.clone());
281        if let Some(existing) = registry.name_to_id.get(&key) {
282            return Self::new(exchange, *existing);
283        }
284        let next = registry
285            .next_per_exchange
286            .entry(exchange)
287            .and_modify(|id| *id = id.saturating_add(1))
288            .or_insert(1);
289        let id = *next;
290        registry.name_to_id.insert(key, id);
291        registry
292            .id_to_name
293            .insert((exchange, id), leak_string(normalized));
294        Self::new(exchange, id)
295    }
296
297    #[must_use]
298    pub fn code(&self) -> &'static str {
299        symbol_code_lookup(self.exchange, self.market_id)
300    }
301
302    pub const fn unspecified() -> Self {
303        Self::new(ExchangeId::UNSPECIFIED, 0)
304    }
305}
306
307impl Default for Symbol {
308    fn default() -> Self {
309        Self::unspecified()
310    }
311}
312
313impl fmt::Display for Symbol {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(f, "{}:{}", self.exchange, self.code())
316    }
317}
318
319impl FromStr for Symbol {
320    type Err = IdentifierParseError;
321
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        let (exchange, symbol) = split_identifier(s, "symbol")?;
324        Ok(Self::from_code(exchange, symbol))
325    }
326}
327
328impl Serialize for Symbol {
329    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
330    where
331        S: Serializer,
332    {
333        serializer.serialize_str(self.as_ref())
334    }
335}
336
337impl<'de> Deserialize<'de> for Symbol {
338    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
339    where
340        D: Deserializer<'de>,
341    {
342        let value = String::deserialize(deserializer)?;
343        value.parse().map_err(D::Error::custom)
344    }
345}
346
347impl From<&str> for Symbol {
348    fn from(value: &str) -> Self {
349        value
350            .parse()
351            .unwrap_or_else(|_| Self::from_code(ExchangeId::UNSPECIFIED, value))
352    }
353}
354
355impl From<String> for Symbol {
356    fn from(value: String) -> Self {
357        Symbol::from(value.as_str())
358    }
359}
360
361impl From<&Symbol> for Symbol {
362    fn from(value: &Symbol) -> Self {
363        *value
364    }
365}
366
367impl AsRef<str> for Symbol {
368    fn as_ref(&self) -> &str {
369        self.code()
370    }
371}
372
373#[derive(Debug, Clone)]
374pub struct IdentifierParseError {
375    msg: String,
376}
377
378impl IdentifierParseError {
379    fn new(kind: &str, raw: impl AsRef<str>) -> Self {
380        Self {
381            msg: format!("invalid {kind} identifier: '{}'", raw.as_ref()),
382        }
383    }
384}
385
386impl fmt::Display for IdentifierParseError {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        f.write_str(&self.msg)
389    }
390}
391
392impl std::error::Error for IdentifierParseError {}
393
394#[derive(Default)]
395struct ExchangeRegistry {
396    name_to_id: HashMap<String, ExchangeId>,
397    id_to_name: HashMap<ExchangeId, &'static str>,
398    next_id: u16,
399}
400
401#[derive(Default)]
402struct AssetRegistry {
403    name_to_id: HashMap<(ExchangeId, String), u32>,
404    id_to_name: HashMap<(ExchangeId, u32), &'static str>,
405    next_per_exchange: HashMap<ExchangeId, u32>,
406}
407
408#[derive(Default)]
409struct SymbolRegistry {
410    name_to_id: HashMap<(ExchangeId, String), u32>,
411    id_to_name: HashMap<(ExchangeId, u32), &'static str>,
412    next_per_exchange: HashMap<ExchangeId, u32>,
413}
414
415fn canonicalize(name: &str) -> String {
416    name.trim().to_ascii_lowercase()
417}
418
419fn canonicalize_asset(code: &str) -> String {
420    code.trim().to_ascii_uppercase()
421}
422
423fn split_identifier<'a>(
424    value: &'a str,
425    kind: &'static str,
426) -> Result<(ExchangeId, &'a str), IdentifierParseError> {
427    let value = value.trim();
428    if value.is_empty() {
429        return Err(IdentifierParseError::new(kind, value));
430    }
431    if let Some((exchange, rest)) = value.split_once(':') {
432        let exchange = exchange.parse()?;
433        let rest = rest.trim();
434        if rest.is_empty() {
435            return Err(IdentifierParseError::new(kind, value));
436        }
437        Ok((exchange, rest))
438    } else {
439        Ok((ExchangeId::UNSPECIFIED, value))
440    }
441}
442
443fn asset_code_lookup(exchange: ExchangeId, id: u32) -> &'static str {
444    if id == 0 {
445        return "UNKNOWN";
446    }
447    let registry = ASSETS.read();
448    registry
449        .id_to_name
450        .get(&(exchange, id))
451        .copied()
452        .unwrap_or_else(|| leak_string(format!("asset#{}", id)))
453}
454
455fn symbol_code_lookup(exchange: ExchangeId, id: u32) -> &'static str {
456    if id == 0 {
457        return "UNKNOWN";
458    }
459    let registry = SYMBOLS.read();
460    registry
461        .id_to_name
462        .get(&(exchange, id))
463        .copied()
464        .unwrap_or_else(|| leak_string(format!("symbol#{}", id)))
465}
466
467fn leak_string(value: String) -> &'static str {
468    Box::leak(value.into_boxed_str())
469}