lexe_common/api/
fiat_rates.rs1use std::{collections::BTreeMap, error::Error, fmt, str::FromStr};
4
5use lexe_std::const_utils::const_result_unwrap;
6#[cfg(any(test, feature = "test-utils"))]
7use proptest_derive::Arbitrary;
8use serde::{Deserialize, Serialize};
9use serde_with::DeserializeFromStr;
10
11use crate::time::TimestampMs;
12
13#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
21#[derive(DeserializeFromStr)]
22pub struct IsoCurrencyCode([u8; 3]);
23
24#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
30#[serde(transparent)]
31#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
32pub struct FiatBtcPrice(pub f64);
33
34#[derive(Debug, PartialEq, Serialize, Deserialize)]
53#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
54pub struct FiatRates {
55 pub timestamp_ms: TimestampMs,
58 pub rates: BTreeMap<IsoCurrencyCode, FiatBtcPrice>,
63}
64
65#[derive(Copy, Clone, Debug)]
67pub enum ParseError {
68 BadLength,
69 BadCharacter,
70}
71
72impl FiatRates {
75 pub fn dummy() -> Self {
76 Self {
77 timestamp_ms: TimestampMs::now(),
78 rates: BTreeMap::from_iter([
79 (IsoCurrencyCode::USD, FiatBtcPrice(67086.56654977065)),
80 (IsoCurrencyCode::EUR, FiatBtcPrice(62965.97545915064)),
81 ]),
82 }
83 }
84}
85
86impl IsoCurrencyCode {
89 pub const USD: Self = const_result_unwrap(Self::try_from_bytes(*b"USD"));
90 pub const EUR: Self = const_result_unwrap(Self::try_from_bytes(*b"EUR"));
91 pub const BTC: Self = const_result_unwrap(Self::try_from_bytes(*b"BTC"));
93
94 #[inline]
95 pub fn as_str(&self) -> &str {
96 unsafe { std::str::from_utf8_unchecked(self.0.as_slice()) }
98 }
99
100 #[inline]
101 const fn try_from_bytes(value: [u8; 3]) -> Result<Self, ParseError> {
102 let [c0, c1, c2] = value;
103 if c0.is_ascii_uppercase()
105 && c1.is_ascii_uppercase()
106 && c2.is_ascii_uppercase()
107 {
108 Ok(Self(value))
109 } else {
110 Err(ParseError::BadCharacter)
111 }
112 }
113}
114
115impl FromStr for IsoCurrencyCode {
116 type Err = ParseError;
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 let inner = <[u8; 3]>::try_from(s.as_bytes())
119 .map_err(|_| ParseError::BadLength)?;
120 Self::try_from_bytes(inner)
121 }
122}
123
124impl fmt::Display for IsoCurrencyCode {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 fmt::Display::fmt(self.as_str(), f)
134 }
135}
136
137impl fmt::Debug for IsoCurrencyCode {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 fmt::Debug::fmt(self.as_str(), f)
140 }
141}
142
143impl Serialize for IsoCurrencyCode {
144 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
145 where
146 S: serde::Serializer,
147 {
148 self.as_str().serialize(serializer)
149 }
150}
151
152impl ParseError {
155 fn as_str(&self) -> &'static str {
156 match *self {
157 Self::BadLength =>
158 "IsoCurrencyCode: must be exactly 3 characters long",
159 Self::BadCharacter =>
160 "IsoCurrencyCode: must be all uppercase ASCII",
161 }
162 }
163}
164
165impl fmt::Display for ParseError {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 f.write_str(self.as_str())
168 }
169}
170
171impl Error for ParseError {}
172
173impl fmt::Debug for FiatBtcPrice {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 self.0.fmt(f)
178 }
179}
180
181#[cfg(any(test, feature = "test-utils"))]
182mod arbitrary_impl {
183 use proptest::{
184 array::uniform3,
185 prelude::Arbitrary,
186 strategy::{BoxedStrategy, Strategy},
187 };
188
189 use super::IsoCurrencyCode;
190
191 impl Arbitrary for IsoCurrencyCode {
192 type Parameters = ();
193 type Strategy = BoxedStrategy<Self>;
194
195 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
196 uniform3(b'A'..=b'Z')
197 .prop_map(|code| IsoCurrencyCode::try_from_bytes(code).unwrap())
198 .boxed()
199 }
200 }
201}
202
203#[cfg(test)]
204mod test {
205 use super::*;
206 use crate::test_utils::roundtrip;
207
208 #[test]
209 fn json_roundtrips() {
210 roundtrip::json_string_roundtrip_proptest::<IsoCurrencyCode>();
211 roundtrip::json_value_roundtrip_proptest::<FiatRates>();
212 }
213}