use crate::models::{Model, XRPLModelException, XRPLModelResult};
use alloc::{
borrow::Cow,
string::{String, ToString},
};
use bigdecimal::BigDecimal;
use core::str::FromStr;
use core::{
convert::{TryFrom, TryInto},
fmt::Display,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub struct XRPAmount<'a>(pub Cow<'a, str>);
impl<'a> Model for XRPAmount<'a> {
fn get_errors(&self) -> XRPLModelResult<()> {
self.0.parse::<u32>()?;
Ok(())
}
}
impl Default for XRPAmount<'_> {
fn default() -> Self {
Self("0".into())
}
}
impl Display for XRPAmount<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'de, 'a> Deserialize<'de> for XRPAmount<'a> {
fn deserialize<D>(deserializer: D) -> XRPLModelResult<XRPAmount<'a>, D::Error>
where
D: serde::Deserializer<'de>,
{
let amount_string = Value::deserialize(deserializer)?;
XRPAmount::try_from(amount_string).map_err(serde::de::Error::custom)
}
}
impl<'a> From<Cow<'a, str>> for XRPAmount<'a> {
fn from(value: Cow<'a, str>) -> Self {
Self(value)
}
}
impl<'a> From<&'a str> for XRPAmount<'a> {
fn from(value: &'a str) -> Self {
Self(value.into())
}
}
impl<'a> From<String> for XRPAmount<'a> {
fn from(value: String) -> Self {
Self(value.into())
}
}
impl<'a> From<BigDecimal> for XRPAmount<'a> {
fn from(value: BigDecimal) -> Self {
Self(value.to_string().into())
}
}
impl<'a> From<f64> for XRPAmount<'a> {
fn from(value: f64) -> Self {
Self(value.to_string().into())
}
}
impl<'a> From<u32> for XRPAmount<'a> {
fn from(value: u32) -> Self {
Self(value.to_string().into())
}
}
impl<'a> TryFrom<Value> for XRPAmount<'a> {
type Error = XRPLModelException;
fn try_from(value: Value) -> XRPLModelResult<Self, Self::Error> {
if !value.is_string() && !value.is_number() {
return Err(XRPLModelException::InvalidValue {
field: "XRPAmount".into(),
expected: "string or number".into(),
found: match &value {
Value::Object(_) => "object".into(),
Value::Array(_) => "array".into(),
Value::Null => "null".into(),
Value::Bool(_) => "boolean".into(),
_ => "unknown".into(),
},
});
}
match serde_json::to_string(&value) {
Ok(amount_string) => {
let amount_string = amount_string.clone().replace("\"", "");
Ok(Self(amount_string.into()))
}
Err(serde_error) => Err(serde_error.into()),
}
}
}
impl<'a> TryInto<f64> for XRPAmount<'a> {
type Error = XRPLModelException;
fn try_into(self) -> XRPLModelResult<f64, Self::Error> {
Ok(self.0.parse::<f64>()?)
}
}
impl<'a> TryInto<u32> for XRPAmount<'a> {
type Error = XRPLModelException;
fn try_into(self) -> XRPLModelResult<u32, Self::Error> {
Ok(self.0.parse::<u32>()?)
}
}
impl<'a> TryInto<BigDecimal> for XRPAmount<'a> {
type Error = XRPLModelException;
fn try_into(self) -> XRPLModelResult<BigDecimal, Self::Error> {
Ok(BigDecimal::from_str(&self.0)?)
}
}
impl<'a> TryInto<Cow<'a, str>> for XRPAmount<'a> {
type Error = XRPLModelException;
fn try_into(self) -> XRPLModelResult<Cow<'a, str>, Self::Error> {
Ok(self.0)
}
}
impl<'a> XRPAmount<'a> {
pub fn checked_cmp(&self, other: &Self) -> XRPLModelResult<core::cmp::Ordering> {
let self_decimal: BigDecimal = <Self as Clone>::clone(self).try_into()?;
let other_decimal: BigDecimal = <Self as Clone>::clone(other).try_into()?;
Ok(self_decimal.cmp(&other_decimal))
}
}
impl<'a> PartialOrd for XRPAmount<'a> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<'a> Ord for XRPAmount<'a> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.checked_cmp(other)
.expect("cannot compare invalid XRPAmount values")
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{format, vec};
use core::cmp::Ordering;
#[test]
fn test_cmp_valid_amounts() {
let amount1 = XRPAmount("100".into());
let amount2 = XRPAmount("200".into());
let amount3 = XRPAmount("100".into());
assert_eq!(amount1.cmp(&amount2), Ordering::Less);
assert_eq!(amount2.cmp(&amount1), Ordering::Greater);
assert_eq!(amount1.cmp(&amount3), Ordering::Equal);
}
#[test]
fn test_cmp_zero() {
let zero = XRPAmount("0".into());
let positive = XRPAmount("100".into());
assert_eq!(zero.cmp(&positive), Ordering::Less);
assert_eq!(positive.cmp(&zero), Ordering::Greater);
}
#[test]
fn test_checked_cmp_invalid_vs_valid_returns_error() {
let valid = XRPAmount("100".into());
let invalid = XRPAmount("not-a-number".into());
assert!(valid.checked_cmp(&invalid).is_err());
assert!(invalid.checked_cmp(&valid).is_err());
}
#[test]
fn test_checked_cmp_both_invalid_returns_error() {
let invalid1 = XRPAmount("not-a-number".into());
let invalid2 = XRPAmount("also-invalid".into());
assert!(invalid1.checked_cmp(&invalid2).is_err());
}
#[test]
#[should_panic(expected = "cannot compare invalid XRPAmount values")]
fn test_cmp_panics_on_malformed() {
let valid = XRPAmount("100".into());
let malformed = XRPAmount("xyz".into());
let _ = valid.cmp(&malformed);
}
#[test]
fn test_partial_ord_consistency() {
let amount1 = XRPAmount("100".into());
let amount2 = XRPAmount("200".into());
assert_eq!(amount1.partial_cmp(&amount2), Some(amount1.cmp(&amount2)));
}
#[test]
fn test_sorting_valid_amounts() {
let mut amounts = vec![
XRPAmount("50".into()),
XRPAmount("100".into()),
XRPAmount("25".into()),
];
amounts.sort();
assert_eq!(amounts[0].0.as_ref(), "25");
assert_eq!(amounts[1].0.as_ref(), "50");
assert_eq!(amounts[2].0.as_ref(), "100");
}
#[test]
fn test_try_from_value_rejects_object() {
let obj_value = serde_json::json!({"key": "value"});
let result = XRPAmount::try_from(obj_value);
assert!(result.is_err(), "Object should be rejected");
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("object"));
}
#[test]
fn test_try_from_value_rejects_array() {
let array_value = serde_json::json!([1, 2, 3]);
let result = XRPAmount::try_from(array_value);
assert!(result.is_err(), "Array should be rejected");
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("array"));
}
#[test]
fn test_try_from_value_rejects_null() {
let null_value = serde_json::Value::Null;
let result = XRPAmount::try_from(null_value);
assert!(result.is_err(), "Null should be rejected");
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("null"));
}
#[test]
fn test_try_from_value_rejects_boolean() {
let bool_value = serde_json::json!(true);
let result = XRPAmount::try_from(bool_value);
assert!(result.is_err(), "Boolean should be rejected");
let error_msg = format!("{}", result.unwrap_err());
assert!(error_msg.contains("boolean"));
}
#[test]
fn test_try_from_value_accepts_string() {
let string_value = serde_json::json!("100");
let result = XRPAmount::try_from(string_value);
assert!(result.is_ok(), "String should be accepted");
assert_eq!(result.unwrap().0.as_ref(), "100");
}
#[test]
fn test_try_from_value_accepts_number() {
let number_value = serde_json::json!(100);
let result = XRPAmount::try_from(number_value);
assert!(result.is_ok(), "Number should be accepted");
}
}