use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Symbol {
base: String,
quote: String,
}
impl Symbol {
pub fn new(base: impl Into<String>, quote: impl Into<String>) -> Self {
Self {
base: base.into().to_ascii_uppercase(),
quote: quote.into().to_ascii_uppercase(),
}
}
#[must_use]
pub fn base(&self) -> &str {
&self.base
}
#[must_use]
pub fn quote(&self) -> &str {
&self.quote
}
#[must_use]
pub fn to_concatenated(&self) -> String {
format!("{}{}", self.base, self.quote)
}
}
impl fmt::Display for Symbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.base, self.quote)
}
}
impl FromStr for Symbol {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let (base, quote) = s
.split_once(['/', '-'])
.ok_or_else(|| Error::InvalidSymbol(s.to_string()))?;
if base.is_empty() || quote.is_empty() {
return Err(Error::InvalidSymbol(s.to_string()));
}
Ok(Self::new(base, quote))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_upper_cases_assets() {
let symbol = Symbol::new("btc", "usdt");
assert_eq!(symbol.base(), "BTC");
assert_eq!(symbol.quote(), "USDT");
}
#[test]
fn display_is_canonical_slash_form() {
assert_eq!(Symbol::new("BTC", "USDT").to_string(), "BTC/USDT");
}
#[test]
fn concatenated_form_has_no_separator() {
assert_eq!(Symbol::new("BTC", "USDT").to_concatenated(), "BTCUSDT");
}
#[test]
fn parses_slash_and_dash_separators() {
assert_eq!(
"BTC/USDT".parse::<Symbol>().unwrap(),
Symbol::new("BTC", "USDT")
);
assert_eq!(
"eth-usd".parse::<Symbol>().unwrap(),
Symbol::new("ETH", "USD")
);
}
#[test]
fn rejects_missing_or_empty_parts() {
assert!("BTCUSDT".parse::<Symbol>().is_err());
assert!("BTC/".parse::<Symbol>().is_err());
assert!("/USDT".parse::<Symbol>().is_err());
}
#[test]
fn round_trips_through_json() {
let symbol = Symbol::new("BTC", "USDT");
let json = serde_json::to_string(&symbol).unwrap();
assert_eq!(serde_json::from_str::<Symbol>(&json).unwrap(), symbol);
}
}