templar_primitives/
strnum.rs

1mod serializable_u256;
2pub use serializable_u256::SerializableU256 as SU256;
3
4#[cfg(any(feature = "borsh", feature = "schemars"))]
5use alloc::format;
6#[cfg(feature = "schemars")]
7use alloc::string::String;
8#[cfg(any(feature = "serde", feature = "schemars"))]
9use alloc::string::ToString;
10use core::ops::Deref;
11
12pub type SU64 = StrNum<u64>;
13pub type SU128 = StrNum<u128>;
14pub type SI64 = StrNum<i64>;
15pub type SI128 = StrNum<i128>;
16
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "serde", serde(transparent))]
19#[cfg_attr(
20    feature = "borsh",
21    derive(borsh::BorshSerialize, borsh::BorshDeserialize, borsh::BorshSchema)
22)]
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
24pub struct StrNum<T>(
25    #[cfg_attr(
26        feature = "serde",
27        serde(
28            bound = "T: ToString + core::str::FromStr, <T as core::str::FromStr>::Err: core::fmt::Display",
29            serialize_with = "ser_de::serialize",
30            deserialize_with = "ser_de::deserialize"
31        )
32    )]
33    pub T,
34);
35
36impl<T> StrNum<T> {
37    pub const fn new(value: T) -> Self {
38        Self(value)
39    }
40}
41
42impl<T> Deref for StrNum<T> {
43    type Target = T;
44
45    fn deref(&self) -> &Self::Target {
46        &self.0
47    }
48}
49
50impl<T> AsRef<T> for StrNum<T> {
51    fn as_ref(&self) -> &T {
52        &self.0
53    }
54}
55
56impl<T> From<T> for StrNum<T> {
57    fn from(value: T) -> Self {
58        Self(value)
59    }
60}
61
62#[cfg(feature = "schemars")]
63impl<T> schemars::JsonSchema for StrNum<T>
64where
65    T: schemars::JsonSchema + ToString + core::str::FromStr,
66{
67    fn schema_name() -> String {
68        format!("StrNum_{}", T::schema_name())
69    }
70
71    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
72        let mut schema = schemars::schema::SchemaObject::default();
73        schema.instance_type = Some(schemars::schema::InstanceType::String.into());
74        schema.metadata().description = Some(format!(
75            "string-serialized wrapper around {}",
76            T::schema_name()
77        ));
78        schema.into()
79    }
80}
81
82#[cfg(feature = "serde")]
83mod ser_de {
84    use alloc::string::{String, ToString};
85    use core::str::FromStr;
86    use serde::{Deserialize, Deserializer, Serialize, Serializer};
87
88    pub fn serialize<S: Serializer, T: ToString>(t: &T, ser: S) -> Result<S::Ok, S::Error> {
89        String::serialize(&t.to_string(), ser)
90    }
91
92    pub fn deserialize<'de, D: Deserializer<'de>, T: FromStr>(d: D) -> Result<T, D::Error>
93    where
94        <T as FromStr>::Err: core::fmt::Display,
95    {
96        let s = String::deserialize(d)?;
97        T::from_str(&s).map_err(serde::de::Error::custom)
98    }
99}
100
101#[cfg(feature = "near")]
102mod near {
103    macro_rules! impl_from_near {
104        ($n: ident, $p: ident) => {
105            impl From<near_sdk::json_types::$n> for super::StrNum<$p> {
106                fn from(value: near_sdk::json_types::$n) -> Self {
107                    Self(value.0)
108                }
109            }
110
111            impl From<super::StrNum<$p>> for near_sdk::json_types::$n {
112                fn from(value: super::StrNum<$p>) -> Self {
113                    Self(value.0)
114                }
115            }
116        };
117    }
118
119    impl_from_near!(U64, u64);
120    impl_from_near!(U128, u128);
121    impl_from_near!(I64, i64);
122    impl_from_near!(I128, i128);
123}
124
125#[cfg(test)]
126#[allow(clippy::unreadable_literal)]
127mod tests {
128    use super::{SI128, SI64, SU128, SU64};
129
130    #[cfg(feature = "serde")]
131    #[rstest::rstest]
132    #[case(SU64::from(42_u64), "\"42\"")]
133    #[case(
134        SU128::from(340282366920938463463374607431768211455_u128),
135        "\"340282366920938463463374607431768211455\""
136    )]
137    #[case(SI64::from(-42_i64), "\"-42\"")]
138    #[case(SI128::from(-170141183460469231731687303715884105728_i128), "\"-170141183460469231731687303715884105728\"")]
139    fn serde_round_trip_string_numbers<T>(#[case] value: T, #[case] expected_json: &str)
140    where
141        T: serde::Serialize + serde::de::DeserializeOwned + core::fmt::Debug + PartialEq,
142    {
143        let serialized = serde_json::to_string(&value).unwrap();
144        assert_eq!(serialized, expected_json);
145
146        let deserialized: T = serde_json::from_str(expected_json).unwrap();
147        assert_eq!(deserialized, value);
148    }
149
150    #[cfg(feature = "serde")]
151    #[test]
152    fn serde_deserialize_malformed_string_numbers() {
153        fn assert_malformed_rejected<T>()
154        where
155            T: serde::de::DeserializeOwned,
156        {
157            for bad in ["\"42x\"", "\"-12abc\""] {
158                assert!(serde_json::from_str::<T>(bad).is_err());
159            }
160        }
161
162        assert_malformed_rejected::<SU64>();
163        assert_malformed_rejected::<SU128>();
164        assert_malformed_rejected::<SI64>();
165        assert_malformed_rejected::<SI128>();
166    }
167
168    #[cfg(feature = "schemars")]
169    #[test]
170    fn schemars_schema_is_string_for_string_numbers() {
171        fn assert_schema_is_string<T>()
172        where
173            T: schemars::JsonSchema,
174        {
175            let schema = schemars::schema_for!(T).schema;
176            assert_eq!(
177                schema.instance_type,
178                Some(schemars::schema::InstanceType::String.into())
179            );
180        }
181
182        assert_schema_is_string::<SU64>();
183        assert_schema_is_string::<SU128>();
184        assert_schema_is_string::<SI64>();
185        assert_schema_is_string::<SI128>();
186    }
187}