use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum AssetIdent {
Id(Uuid),
Symbol(String),
}
impl AssetIdent {
pub fn as_path_segment(&self) -> Result<String> {
match self {
Self::Id(id) => Ok(id.to_string()),
Self::Symbol(symbol) => crate::types::path::segment(symbol),
}
}
#[must_use]
pub fn id(&self) -> Option<Uuid> {
match self {
Self::Id(id) => Some(*id),
Self::Symbol(_) => None,
}
}
#[must_use]
pub fn symbol(&self) -> Option<&str> {
match self {
Self::Symbol(symbol) => Some(symbol),
Self::Id(_) => None,
}
}
}
impl fmt::Display for AssetIdent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Symbol(symbol) => f.write_str(symbol),
Self::Id(id) => write!(f, "{id}"),
}
}
}
impl From<Uuid> for AssetIdent {
fn from(id: Uuid) -> Self {
Self::Id(id)
}
}
impl From<String> for AssetIdent {
fn from(value: String) -> Self {
match Uuid::parse_str(&value) {
Ok(id) => Self::Id(id),
Err(_) => Self::Symbol(value),
}
}
}
impl From<&str> for AssetIdent {
fn from(value: &str) -> Self {
Self::from(value.to_owned())
}
}
impl FromStr for AssetIdent {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self::from(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
const UUID_STR: &str = "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415";
#[test]
fn symbols_stay_symbols() {
let ident = AssetIdent::from("AAPL");
assert_eq!(ident, AssetIdent::Symbol("AAPL".to_owned()));
assert_eq!(ident.symbol(), Some("AAPL"));
assert_eq!(ident.id(), None);
}
#[test]
fn a_uuid_shaped_string_is_parsed_as_an_id_not_a_symbol() {
let ident = AssetIdent::from(UUID_STR);
assert_eq!(ident.id(), Some(Uuid::parse_str(UUID_STR).unwrap()));
assert_eq!(ident.symbol(), None);
}
#[test]
fn crypto_pairs_are_symbols_not_ids() {
assert_eq!(
AssetIdent::from("BTC/USD"),
AssetIdent::Symbol("BTC/USD".to_owned())
);
}
#[test]
fn path_segment_renders_both_forms() {
assert_eq!(AssetIdent::from("AAPL").as_path_segment().unwrap(), "AAPL");
assert_eq!(
AssetIdent::from(UUID_STR).as_path_segment().unwrap(),
UUID_STR
);
}
#[test]
fn a_crypto_pair_displays_readably_and_encodes_for_the_path() {
let ident = AssetIdent::from("BTC/USD");
assert_eq!(ident.to_string(), "BTC/USD");
assert_eq!(ident.as_path_segment().unwrap(), "BTC%2FUSD");
}
#[test]
fn a_dot_segment_symbol_is_refused_rather_than_sent() {
assert!(
AssetIdent::Symbol("..".to_owned())
.as_path_segment()
.is_err()
);
assert!(AssetIdent::Symbol(String::new()).as_path_segment().is_err());
}
#[test]
fn round_trips_through_json_in_both_forms() {
for ident in [AssetIdent::from("AAPL"), AssetIdent::from(UUID_STR)] {
let json = serde_json::to_string(&ident).unwrap();
let decoded: AssetIdent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, ident);
}
}
}