use smol_str::SmolStr;
use std::convert::TryFrom;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Asset(SmolStr);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AssetError {
Empty,
}
impl Display for AssetError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => formatter.write_str("asset must not be empty"),
}
}
}
impl std::error::Error for AssetError {}
impl Asset {
pub fn new(value: impl AsRef<str>) -> Result<Self, AssetError> {
let value = value.as_ref();
if value.trim().is_empty() {
return Err(AssetError::Empty);
}
Ok(Self(SmolStr::new(value)))
}
}
impl TryFrom<&str> for Asset {
type Error = AssetError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for Asset {
type Error = AssetError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl AsRef<str> for Asset {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl Deref for Asset {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_str()
}
}
impl Display for Asset {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.0.as_str())
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use super::{Asset, AssetError};
#[test]
fn new_and_deref_work() {
let from_str = Asset::new("SPX").expect("asset code must be valid");
let from_string = Asset::new(String::from("SPX")).expect("asset code must be valid");
assert_eq!(&*from_str, "SPX");
assert_eq!(from_str.as_ref(), "SPX");
assert_eq!(from_str, from_string);
}
#[test]
fn new_rejects_empty_and_whitespace() {
assert_eq!(Asset::new(""), Err(AssetError::Empty));
assert_eq!(Asset::new(" "), Err(AssetError::Empty));
}
#[test]
fn display_outputs_inner_value() {
let asset = Asset::new("SPX").expect("asset code must be valid");
assert_eq!(format!("{asset}"), "SPX");
}
#[test]
fn try_from_validates_asset_code() {
let from_str = Asset::try_from("USD").expect("asset code must be valid");
let from_string = Asset::try_from(String::from("EUR")).expect("asset code must be valid");
assert_eq!(from_str.as_ref(), "USD");
assert_eq!(from_string.as_ref(), "EUR");
assert_eq!(Asset::try_from(""), Err(AssetError::Empty));
assert_eq!(Asset::try_from(String::from(" ")), Err(AssetError::Empty));
}
#[test]
fn asset_error_display_is_stable() {
assert_eq!(AssetError::Empty.to_string(), "asset must not be empty");
}
}