oanda_rs/models/
primitives.rs1use std::fmt;
8use std::str::FromStr;
9
10use rust_decimal::Decimal;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13use super::serde_util::decimal_from_str_or_number;
14
15macro_rules! string_newtype {
17 ($(#[$meta:meta])* $name:ident) => {
18 $(#[$meta])*
19 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20 #[serde(transparent)]
21 pub struct $name(pub String);
22
23 impl $name {
24 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28 }
29
30 impl fmt::Display for $name {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 f.write_str(&self.0)
33 }
34 }
35
36 impl From<String> for $name {
37 fn from(value: String) -> Self {
38 $name(value)
39 }
40 }
41
42 impl From<&str> for $name {
43 fn from(value: &str) -> Self {
44 $name(value.to_owned())
45 }
46 }
47
48 impl FromStr for $name {
49 type Err = std::convert::Infallible;
50
51 fn from_str(s: &str) -> Result<Self, Self::Err> {
52 Ok($name(s.to_owned()))
53 }
54 }
55 };
56}
57
58string_newtype! {
59 AccountId
62}
63
64string_newtype! {
65 TransactionId
67}
68
69string_newtype! {
70 OrderId
72}
73
74string_newtype! {
75 TradeId
77}
78
79string_newtype! {
80 ClientId
84}
85
86string_newtype! {
87 ClientTag
89}
90
91string_newtype! {
92 ClientComment
94}
95
96string_newtype! {
97 RequestId
100}
101
102string_newtype! {
103 Currency
105}
106
107string_newtype! {
108 OrderSpecifier
112}
113
114string_newtype! {
115 TradeSpecifier
118}
119
120impl OrderSpecifier {
121 pub fn from_client_id(id: impl AsRef<str>) -> Self {
124 OrderSpecifier(format!("@{}", id.as_ref()))
125 }
126}
127
128impl From<&OrderId> for OrderSpecifier {
129 fn from(id: &OrderId) -> Self {
130 OrderSpecifier(id.0.clone())
131 }
132}
133
134impl From<OrderId> for OrderSpecifier {
135 fn from(id: OrderId) -> Self {
136 OrderSpecifier(id.0)
137 }
138}
139
140impl TradeSpecifier {
141 pub fn from_client_id(id: impl AsRef<str>) -> Self {
144 TradeSpecifier(format!("@{}", id.as_ref()))
145 }
146}
147
148impl From<&TradeId> for TradeSpecifier {
149 fn from(id: &TradeId) -> Self {
150 TradeSpecifier(id.0.clone())
151 }
152}
153
154impl From<TradeId> for TradeSpecifier {
155 fn from(id: TradeId) -> Self {
156 TradeSpecifier(id.0)
157 }
158}
159
160macro_rules! decimal_newtype {
163 ($(#[$meta:meta])* $name:ident) => {
164 $(#[$meta])*
165 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
166 pub struct $name(pub Decimal);
167
168 impl $name {
169 pub fn value(&self) -> Decimal {
171 self.0
172 }
173 }
174
175 impl Serialize for $name {
176 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
177 serializer.collect_str(&self.0)
178 }
179 }
180
181 impl<'de> Deserialize<'de> for $name {
182 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
183 decimal_from_str_or_number(deserializer).map($name)
184 }
185 }
186
187 impl fmt::Display for $name {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 fmt::Display::fmt(&self.0, f)
190 }
191 }
192
193 impl From<Decimal> for $name {
194 fn from(value: Decimal) -> Self {
195 $name(value)
196 }
197 }
198
199 impl From<$name> for Decimal {
200 fn from(value: $name) -> Self {
201 value.0
202 }
203 }
204
205 impl From<i64> for $name {
206 fn from(value: i64) -> Self {
207 $name(Decimal::from(value))
208 }
209 }
210
211 impl From<i32> for $name {
212 fn from(value: i32) -> Self {
213 $name(Decimal::from(value))
214 }
215 }
216
217 impl From<u32> for $name {
218 fn from(value: u32) -> Self {
219 $name(Decimal::from(value))
220 }
221 }
222
223 impl FromStr for $name {
224 type Err = rust_decimal::Error;
225
226 fn from_str(s: &str) -> Result<Self, Self::Err> {
227 s.parse::<Decimal>()
228 .or_else(|_| Decimal::from_scientific(s))
229 .map($name)
230 }
231 }
232
233 impl TryFrom<f64> for $name {
234 type Error = rust_decimal::Error;
235
236 fn try_from(value: f64) -> Result<Self, Self::Error> {
237 Decimal::try_from(value).map($name)
238 }
239 }
240 };
241}
242
243decimal_newtype! {
244 DecimalNumber
246}
247
248decimal_newtype! {
249 AccountUnits
252}
253
254decimal_newtype! {
255 PriceValue
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
267#[serde(transparent)]
268pub struct DateTime(pub String);
269
270impl DateTime {
271 pub fn as_str(&self) -> &str {
273 &self.0
274 }
275
276 pub fn to_utc(&self) -> Option<chrono::DateTime<chrono::Utc>> {
280 let s = self.0.as_str();
281 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
282 return Some(dt.with_timezone(&chrono::Utc));
283 }
284 let (secs, frac) = match s.split_once('.') {
286 Some((secs, frac)) => (secs, frac),
287 None => (s, ""),
288 };
289 let secs: i64 = secs.parse().ok()?;
290 let nanos: u32 = if frac.is_empty() {
291 0
292 } else if frac.len() <= 9 && frac.chars().all(|c| c.is_ascii_digit()) {
293 frac.parse::<u32>().ok()? * 10u32.pow(9 - frac.len() as u32)
294 } else {
295 return None;
296 };
297 chrono::DateTime::from_timestamp(secs, nanos)
298 }
299}
300
301impl fmt::Display for DateTime {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 f.write_str(&self.0)
304 }
305}
306
307impl From<String> for DateTime {
308 fn from(value: String) -> Self {
309 DateTime(value)
310 }
311}
312
313impl From<&str> for DateTime {
314 fn from(value: &str) -> Self {
315 DateTime(value.to_owned())
316 }
317}
318
319impl From<chrono::DateTime<chrono::Utc>> for DateTime {
320 fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
321 DateTime(value.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
328pub enum AcceptDatetimeFormat {
329 #[serde(rename = "UNIX")]
331 Unix,
332 #[default]
334 #[serde(rename = "RFC3339")]
335 Rfc3339,
336}
337
338impl AcceptDatetimeFormat {
339 pub fn as_header_value(self) -> &'static str {
341 match self {
342 AcceptDatetimeFormat::Unix => "UNIX",
343 AcceptDatetimeFormat::Rfc3339 => "RFC3339",
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn decimal_newtype_serializes_as_string() {
354 let v = DecimalNumber::from_str("1.2345").unwrap();
355 assert_eq!(serde_json::to_string(&v).unwrap(), r#""1.2345""#);
356 }
357
358 #[test]
359 fn decimal_newtype_deserializes_from_number() {
360 let v: PriceValue = serde_json::from_str("1.1050").unwrap();
361 assert_eq!(v.to_string(), "1.105");
362 }
363
364 #[test]
365 fn decimal_newtype_roundtrips_string() {
366 let v: AccountUnits = serde_json::from_str(r#""-43.2100""#).unwrap();
367 assert_eq!(serde_json::to_string(&v).unwrap(), r#""-43.2100""#);
368 }
369
370 #[test]
371 fn datetime_parses_rfc3339() {
372 let dt = DateTime::from("2024-06-14T12:01:32.123456Z");
373 let parsed = dt.to_utc().unwrap();
374 assert_eq!(parsed.timestamp(), 1718366492);
375 assert_eq!(parsed.timestamp_subsec_micros(), 123456);
376 }
377
378 #[test]
379 fn datetime_parses_unix_with_fraction() {
380 let dt = DateTime::from("1718366492.123456000");
381 let parsed = dt.to_utc().unwrap();
382 assert_eq!(parsed.timestamp(), 1718366492);
383 assert_eq!(parsed.timestamp_subsec_micros(), 123456);
384 }
385
386 #[test]
387 fn datetime_parses_unix_without_fraction() {
388 let dt = DateTime::from("1718366492");
389 assert_eq!(dt.to_utc().unwrap().timestamp(), 1718366492);
390 }
391
392 #[test]
393 fn datetime_rejects_garbage() {
394 assert!(DateTime::from("not a date").to_utc().is_none());
395 }
396
397 #[test]
398 fn order_specifier_from_client_id() {
399 assert_eq!(OrderSpecifier::from_client_id("my-id").as_str(), "@my-id");
400 assert_eq!(OrderSpecifier::from(OrderId::from("42")).as_str(), "42");
401 }
402}