templar_primitives/
strnum.rs1mod 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}