wickra_exchange_core/
symbol.rs1use crate::error::{Error, Result};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::str::FromStr;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct Symbol {
21 base: String,
22 quote: String,
23}
24
25impl Symbol {
26 pub fn new(base: impl Into<String>, quote: impl Into<String>) -> Self {
28 Self {
29 base: base.into().to_ascii_uppercase(),
30 quote: quote.into().to_ascii_uppercase(),
31 }
32 }
33
34 #[must_use]
36 pub fn base(&self) -> &str {
37 &self.base
38 }
39
40 #[must_use]
42 pub fn quote(&self) -> &str {
43 &self.quote
44 }
45
46 #[must_use]
50 pub fn to_concatenated(&self) -> String {
51 format!("{}{}", self.base, self.quote)
52 }
53}
54
55impl fmt::Display for Symbol {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}/{}", self.base, self.quote)
58 }
59}
60
61impl FromStr for Symbol {
62 type Err = Error;
63
64 fn from_str(s: &str) -> Result<Self> {
68 let (base, quote) = s
69 .split_once(['/', '-'])
70 .ok_or_else(|| Error::InvalidSymbol(s.to_string()))?;
71 if base.is_empty() || quote.is_empty() {
72 return Err(Error::InvalidSymbol(s.to_string()));
73 }
74 Ok(Self::new(base, quote))
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn new_upper_cases_assets() {
84 let symbol = Symbol::new("btc", "usdt");
85 assert_eq!(symbol.base(), "BTC");
86 assert_eq!(symbol.quote(), "USDT");
87 }
88
89 #[test]
90 fn display_is_canonical_slash_form() {
91 assert_eq!(Symbol::new("BTC", "USDT").to_string(), "BTC/USDT");
92 }
93
94 #[test]
95 fn concatenated_form_has_no_separator() {
96 assert_eq!(Symbol::new("BTC", "USDT").to_concatenated(), "BTCUSDT");
97 }
98
99 #[test]
100 fn parses_slash_and_dash_separators() {
101 assert_eq!(
102 "BTC/USDT".parse::<Symbol>().unwrap(),
103 Symbol::new("BTC", "USDT")
104 );
105 assert_eq!(
106 "eth-usd".parse::<Symbol>().unwrap(),
107 Symbol::new("ETH", "USD")
108 );
109 }
110
111 #[test]
112 fn rejects_missing_or_empty_parts() {
113 assert!("BTCUSDT".parse::<Symbol>().is_err());
114 assert!("BTC/".parse::<Symbol>().is_err());
115 assert!("/USDT".parse::<Symbol>().is_err());
116 }
117
118 #[test]
119 fn round_trips_through_json() {
120 let symbol = Symbol::new("BTC", "USDT");
121 let json = serde_json::to_string(&symbol).unwrap();
122 assert_eq!(serde_json::from_str::<Symbol>(&json).unwrap(), symbol);
123 }
124}